برنامه Android برای شناسایی VPN و proxy روی دستگاه. این پروژه روش مبتنی بر منطق روسکومنادزور برای تشخیص ابزارهای دور زدن مسدودسازی را پیادهسازی میکند.
حداقل نسخه Android: 8.0 (API 26).
این پروژه روشهای شناسایی VPN و proxy روی دستگاههای 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)
- پنهانسازی proxyهای localhost (چگونه از شناسایی از طریق
/proc/net/tcpو اسکن پورت جلوگیری کنیم) - دور زدن بررسیهای بومی/native (مقابله با بررسیهای مبتنی بر JNI از طریق
/proc/self/maps،getifaddrs()وdlsym) - پنهانسازی برنامههای نصبشده (مخفی کردن بستههای برنامه VPN از
PackageManager)
اگر در این زمینهها تخصص دارید، لطفاً یک Issue یا Pull Request باز کنید، یا در چت Matrix/Telegram روش خود را همراه با شرایط کاربرد و محدودیتهای آن شرح دهید. هر اطلاعاتی ارزشمند است — از ایدههای تئوری تا PoCهای کاربردی.
ماژولهای مستقل بررسی بهصورت موازی اجرا میشوند. نتیجه نهایی در VerdictEngine محاسبه میشود.
IpComparisonChecker در نتیجه ذخیره میشود و در رابط کاربری بهعنوان یک بلوک تشخیصی نمایش داده میشود، اما در نسخه فعلی در VerdictEngine نقشی ندارد.
VpnCheckRunner
├── GeoIpChecker — GeoIP + نشانههای hosting/proxy
├── IpComparisonChecker — checkerهای IP برای RU/غیر RU (تشخیصی)
├── DirectSignsChecker — NetworkCapabilities، system proxy، برنامههای VPN نصبشده
├── IndirectSignsChecker — اینترفیسها، routeها، 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، underlying-network leak
├── RttTriangulationChecker — SNITCH (β): مثلثبندی RTT با هاستهای RU/خارجی
└── NativeSignsChecker — بررسیهای JNI (مسیرها، هوکها، root)
└── VerdictEngine — منطق نتیجه نهایی
منابع:
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 |
رأی اکثریت بین پاسخهای سازگار برای یک IP یکسان (ipapi.is, iplocate.io) استفاده میشود |
اگر بیشتر منابع سازگار hosting=true بگویند، detected = true |
proxy |
از ارائهدهندگان HTTPS سازگار (ipapi.is, iplocate.io) استفاده میشود |
اگر حداقل یک ارائهدهنده سازگار proxy/VPN/Tor گزارش کند، detected = true |
country, isp, org, as, query |
از ipapi.is گرفته میشوند و فیلدهای خالی فقط برای IP سازگار از iplocate.io پر میشوند |
اثر مستقیم ندارند |
نتیجه نهایی دسته:
detected = isHosting || isProxyneedsReview = foreignIp && !isHosting && !isProxy
timeout اتصال و خواندن برای درخواستهای HTTP(S): ده ثانیه. GeoIpChecker فقط از ارائهدهندگان HTTPS استفاده میکند و تنها وقتی خطا برمیگرداند که هیچ ارائهدهنده GeoIP دادهای برنگرداند.
این ماژول پاسخ checkerهای عمومی IP در RU و غیر RU را مقایسه میکند. این بخش تشخیصی است: در 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 متفاوت برگردانند؛- خطاهای مورد انتظار برای endpointهای IPv6 میتوانند نادیده گرفته شوند و اجماع IPv4 را نشکنند.
نشانههای سیستمی بدون اسکن فعال localhost.
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 بررسی میشوند.
منابع:
System.getProperty("http.proxyHost")با fallback بهProxy.getDefaultHost()System.getProperty("http.proxyPort")با fallback بهProxy.getDefaultPort()System.getProperty("socksProxyHost")System.getProperty("socksProxyPort")
منطق:
| وضعیت | نتیجه |
|---|---|
| host وجود ندارد | proxy پیکربندینشده در نظر گرفته میشود |
| host وجود دارد اما پورت نامعتبر است | needsReview = true |
| host و پورت هر دو معتبرند | detected = true |
| پورت جزو پورتهای شناختهشده proxy است | یک finding اضافی اضافه میشود |
پورتهای شناختهشده proxy: 80, 443, 1080, 3127, 3128, 4080, 5555, 7000, 7044, 8000, 8080, 8081, 8082, 8888, 9000, 9050, 9051, 9150, 12345 و همچنین بازه 16000..16100.
ماژول دو منبع را بررسی میکند:
- امضاهای شناختهشده package از
VpnAppCatalog؛ - برنامههایی که از طریق
PackageManager.queryIntentServices، رابطVpnService.SERVICE_INTERFACEرا اعلان میکنند. - برنامه در نام خود «VPN» دارد (البته این ۱۰۰٪ تضمین نمیکند که VPN باشد)
اینها سیگنالهای تشخیصی نصب برنامه یا اعلان
VpnServiceهستند، نه تأیید یک تونل فعال. تطبیقها دسته را بهneedsReviewمیبرند، اما بهتنهایی باعثDirectSignsChecker.detected = trueنمیشوند.
روی ConnectivityManager.getNetworkCapabilities(activeNetwork).toString() بررسی میشود که آیا رشته NOT_VPN وجود دارد یا نه.
| نتیجه | خروجی |
|---|---|
NOT_VPN وجود دارد |
عادی |
NOT_VPN وجود ندارد |
detected = true |
API: NetworkInterface.getNetworkInterfaces(). فقط اینترفیسهای فعال (isUp) بررسی میشوند.
الگوهای اینترفیس شبیه VPN:
tun\d+tap\d+wg\d+ppp\d+ipsec.*
هر اینترفیس فعالی که با این الگوها تطبیق کند، detected = true میدهد.
منطق:
| شرط | نتیجه |
|---|---|
اینترفیس شبیه VPN با MTU در بازه 1..1499 |
detected = true |
اینترفیس فعال غیر استاندارد (نه wlan.*, rmnet.*, eth.*, lo) با MTU در بازه 1..1499 |
detected = true |
منابع داده:
- در اولویت اول
LinkProperties.routesاز Android API؛ - fallback: فایل
/proc/net/routeاگر از طریق API نتوان default route را بهدست آورد.
موارد شناسایی:
- default route از طریق اینترفیس غیر استاندارد؛
- routeهای non-default اختصاصی از طریق VPN/اینترفیس غیر استاندارد؛
- الگوی split tunneling: همزمان routeهای tunnel و یک default route معمولی از طریق شبکه استاندارد دیده میشوند.
اگر default route از طریق wlan.*, rmnet.*, eth.*, lo باشد و خود شبکه VPN نباشد، حالت عادی محسوب میشود.
API: ConnectivityManager.getLinkProperties(activeNetwork).dnsServers
DNS همراه با snapshot شبکههای underlying ارزیابی میشود، اگر آنها در دسترس باشند.
| سیگنال | نتیجه |
|---|---|
loopback DNS (127.x.x.x, ::1) |
detected = true |
| private DNS که از همان private/ULA subnet شبکه 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) |
informational |
بررسی میشود:
- ابزارهای proxy-only نصبشده از
VpnAppCatalogبا سیگنالLOCAL_PROXYو بدونVPN_SERVICE؛ - listenerهای محلی در
/proc/net/tcp,/proc/net/tcp6,/proc/net/udp,/proc/net/udp6روی پورتهای شناختهشده proxy؛ - تعداد زیاد localhost listener روی پورتهای بالا.
منطق:
- listener روی localhost proxy port شناختهشده،
detected = trueمیدهد؛ - وجود ابزار proxy-only یا تعداد زیاد localhost listener،
needsReview = trueمیدهد.
یک محدودیت جداگانه هم ثبت میشود: بررسی processها، iptables/pf و گواهیهای سیستمی بدون root/privileged access ناقص هستند.
فقط Android 12+ (API 31+). دستور dumpsys vpn_management اجرا میشود.
اگر parser (VpnDumpsysParser) رکوردهای فعال VPN را پیدا کند، آنها detected = true میدهند. از رکوردها package استخراج میشود و با VpnAppCatalog تطبیق داده میشود:
- package شناختهشده: اطمینان بالا؛
- package ناشناخته:
detected = trueو همزمانneedsReview = true.
خروجی خالی، Permission Denial یا دردسترسنبودن سرویس بهعنوان عدم شناسایی در نظر گرفته میشود.
دستور dumpsys activity services android.net.VpnService اجرا میشود.
اگر VpnService فعال پیدا شود، activeApps و evidence ساخته میشوند:
- package شناختهشده از catalog: اطمینان بالا؛
- package ناشناخته:
detected = trueوneedsReview = true.
خروجی خالی یا نبودن رکوردهای VpnService باعث شناسایی نمیشود.
این ماژول نشانههایی را جمع میکند که تأیید میکنند دستگاه واقعاً در روسیه قرار دارد یا برعکس، سیگنالهای تلفنی غیرعادی به نظر میرسند.
منابع:
TelephonyManager.networkOperator,networkCountryIso,networkOperatorNameTelephonyManager.simOperator,simCountryIso,isNetworkRoamingrequestCellInfoUpdate/allCellInfoWifiManager.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 |
finding داخلی network_mcc_ru:true اضافه میشود |
اگر BeaconDB/reverse geocode مقدار RU برگرداند |
cell_country_ru:true و location_country_ru:true اضافه میشوند |
networkMcc != 250 |
needsReview = true |
| نبود مجوز یا radio data | informational |
در پیادهسازی فعلی، LocationSignalsChecker.detected همیشه false است. نقش اصلی آن در VerdictEngine تأیید روسیه و تقویت سیگنال GeoIP خارجی است.
سه بررسی بهصورت موازی اجرا میشوند:
ProxyScannerXrayApiScannerUnderlyingNetworkProber
آدرسهای 127.0.0.1 و ::1 اسکن میشوند.
حالتها:
| حالت | توضیح |
|---|---|
AUTO |
ابتدا پورتهای رایج، سپس کل بازه |
MANUAL |
بررسی یک پورت مشخص |
پورتهای رایج در AUTO از VpnAppCatalog.localhostProxyPorts ساخته میشوند و علاوه بر آن 1081, 7890, 7891 نیز اضافه میشوند.
اسکن کامل:
- بازه
1024..65535 - موازیسازی
200 - timeout اتصال
80 ms - timeout خواندن
120 ms
فقط proxyهای بدون احراز هویت شناسایی میشوند:
| نوع | روش شناسایی |
|---|---|
SOCKS5 |
greeting 0x05 0x01 0x00 و پاسخ 0x05 0x00 |
HTTP CONNECT |
CONNECT ifconfig.me:443 HTTP/1.1 و پاسخ HTTP/1.x 200 |
open localhost proxy بهتنهایی bypass تأییدشده محسوب نمیشود: فقط بهصورت needsReview ثبت میشود. تأیید bypass فقط وقتی انجام میشود که هم IP مستقیم و هم IP از طریق proxy بهدست بیاید و با هم متفاوت باشند.
علاوه بر این:
- اگر
SOCKS5پیدا شود، ولی دریافت HTTP IP از طریق آن ناموفق باشد و پورت شبیه Xray نباشد،MtProtoProberاجرا میشود؛ - MTProto probe موفق فقط یک finding اطلاعاتی اضافه میکند و روی verdict نهایی اثری ندارد.
آدرسهای 127.0.0.1 و ::1 اسکن میشوند.
پارامترها:
- بازه
1024..65535 - موازیسازی
100 - TCP connect timeout برابر
200 ms - gRPC deadline برابر
2000 msبا retry روی deadline بزرگتر
این بررسی از طریق raw HTTP/2 preface انجام نمیشود، بلکه از یک فراخوانی واقعی gRPC یعنی HandlerServiceGrpc.listOutbounds(...) استفاده میکند.
در صورت موفقیت:
- endpoint مقدار
detected = trueمیدهد؛ - در findings حداکثر 10 خلاصه از outboundها (
tag,protocol,address,port,sni) و یک شمارنده برای بقیه اضافه میشود.
اگر VPN روی دستگاه فعال باشد، ماژول:
- تمام
ConnectivityManager.allNetworksرا پیمایش میکند؛ - یک شبکه دارای اینترنت ولی بدون
TRANSPORT_VPNپیدا میکند؛ - درخواستهای HTTP(S) را به آن شبکه bind میکند؛
- IP عمومی را از طریق
ifconfig.me,checkip.amazonaws.com,ipv4-internet.yandex.net,ipv6-internet.yandex.netدرخواست میکند.
اگر هنگام فعال بودن VPN، شبکه underlying در دسترس باشد، این وضعیت بهعنوان VPN gateway leak تعبیر میشود و detected = true میدهد.
نتیجه نهایی دسته:
detected = confirmed split tunnel || xrayApiFound || vpnGatewayLeak || vpnNetworkBinding- اگر open proxy پیدا شود ولی bypass تأیید نشود،
needsReview = true
درخواستهای HTTPS را به redirectorها و endpointهای شناختهشده trace (مانند Google Video, Cloudflare trace, Meduza) ارسال میکند تا ببیند چه IP عمومی یا متادیتا شبکهای نمایش داده میشود. تفاوت در پاسخها میتواند نشاندهنده پروکسی یا تونل باشد.
دسترسیپذیری UDP/STUN را در endpointهای جهانی و منطقهای بررسی میکند و دسترسیپذیری TCP MTProto را از طریق پروکسیهای محلی آزمایش میکند. این میتواند IPهای عمومی نگاشتشده (mapped) یا نشتهای شبکههای زیرین که تونلهای معمولی را دور میزنند، آشکار کند.
پینگ ICMP به مجموعهای از هاستهای روسی و خارجی ارسال میکند و میانههای زمان رفتوبرگشت را مقایسه میکند.
اهداف روسی: yandex.ru, mail.ru, vk.com, sberbank.ru, gosuslugi.ru.
اهداف خارجی: facebook.com, github.com, twitter.com, reddit.com, instagram.com.
منطق:
- اگر میانه RTT به هاستهای RU از آستانه (
80 ms) بیشتر باشد، دستگاه احتمالاً در روسیه نیست؛ - jitter بالا (> 60 ms) اطمینان به نتیجه را کاهش میدهد؛
- نتیجه verdict را به
NEEDS_REVIEWارتقا میدهد، اما بهتنهاییDETECTEDتولید نمیکند.
این بررسی اختیاری است و بهطور پیشفرض غیرفعال است.
بررسیهای JNI سطح پایین را مستقیماً از C++ انجام میدهد:
- لیست کردن اینترفیسهای بومی و بررسیهای
getifaddrs() - پردازش مستقیم
/proc/net/route - اسکن کردن متنی
/proc/self/mapsبرای نشانگرهای شناختهشده hook - بررسی یکپارچگی تفکیک نمادهای
libc - تشخیص Root (فایلهای باینری su، ویژگیهای magisk، حالت selinux، دسترسی rw مسیر /system و غیره)
یافتههای سطح بومی میتوانند به حالتهای needsReview یا نشانههای عمومی غیرمستقیم مسیریابی ترجمه شوند.
VerdictEngine از تمام بلوکهای جمعآوریشده به یک اندازه استفاده نمیکند.
ابتدا قواعد بدون شرط اعمال میشوند:
- اگر در bypass-evidence مقدار
SPLIT_TUNNEL_BYPASSوجود داشته باشد،DETECTED. - اگر
XRAY_APIپیدا شود،DETECTED. - اگر
VPN_GATEWAY_LEAKپیدا شود،DETECTED. - اگر سیگنالهای مکان روسیه را تأیید کنند (
network_mcc_ru:true,cell_country_ru:trueیاlocation_country_ru:true) و همزمانGeoIPسیگنال خارجی بدهد،DETECTED.
سپس یک ماتریس محاسبه میشود:
geoMatrixHit= سیگنال GeoIP خارجی (geoIp.needsReviewیا evidence از نوعGEO_IP)directMatrixHit= evidence ازDIRECT_NETWORK_CAPABILITIESیاSYSTEM_PROXYindirectMatrixHit= evidence ازINDIRECT_NETWORK_CAPABILITIES,ACTIVE_VPN,NETWORK_INTERFACE,ROUTING,DNS,PROXY_TECHNICAL_SIGNAL
ترکیبها:
| Geo | Direct | Indirect | Verdict |
|---|---|---|---|
| خیر | خیر | خیر | NOT_DETECTED |
| خیر | بله | خیر | NOT_DETECTED |
| خیر | خیر | بله | NOT_DETECTED |
| بله | خیر | خیر | NEEDS_REVIEW |
| خیر | بله | بله | NEEDS_REVIEW |
| هر ترکیب دیگر | DETECTED |
نکات:
IpComparisonCheckerفعلاً درVerdictEngineاستفاده نمیشود؛- سیگنالهای
INSTALLED_APPوVPN_SERVICE_DECLARATIONنیز وارد ماتریس نمیشوند و فقط نقش تشخیصی دارند؛ - نشتهای عملیاتی (actionable) از
CallTransportCheckerیا یافتههای نیازمند بررسی ازNativeSignsChecker(مانند نشانگرهای hook) وضعیت را ازNOT_DETECTEDبهNEEDS_REVIEWارتقا میدهند.
نیازمندیها: JDK 17+ و Android SDK با Build Tools برای API 36.
./gradlew assembleDebugrunetfreedom — بابت per-app-split-bypass-poc که تشخیص per-app split bypass بر پایه آن پیادهسازی شده است.