تشخیص VPN در اندروید: چرا tun0 و ping گمراهکنندهاند
نام رابط یک قرارداد است؛ یک ping ثابت میکند که یک میزبان در دسترس بوده است. از NetworkCapabilities بپرسید شبکه چه چیزی اعلام میکند، سپس به callbackها واکنش دهید.
بررسیای که بیشتر برنامههای اندروید برای تشخیص VPN به کار میبرند یک نام رابط است و هرگز بررسی VPN نبوده است. گزارش میدهد که رابطی با رشتهای که توسعهدهنده انتظار داشت وجود دارد، که روی یک دستگاه یعنی OpenVPN و روی دستگاهی دیگر یعنی چیزی کاملاً بیربط. ping هم همان نوع شاهد است: ثابت میکند یک مقصد پاسخ داده، نه اینکه تونلی بسته را حمل کرده است. آنچه پلتفرم واقعاً ارائه میدهد مجموعهای از شبکهها و ترانسپورتهایی است که هر یک اعلام میکند، و این پرسش دیگری با پاسخ دیگری است.
افسانه: اگر tun0 وجود دارد، VPN فعال است
برنامههای VPN اندروید پیادهسازیهای VpnService هستند که یک توصیفگر فایل tun به سیستم میدهند. نام رابط از کسی میآید که آن را ساخته است: OpenVPN معمولاً tun0 تولید میکند، PPTP و L2TP هر دو ppp0 تولید میکنند، WireGuard رابط خود را به نام تونل مینامد، و تونل IPsec ممکن است اصلاً نام متعارفی نداشته باشد. بنابراین خواندن /proc/net/dev یا شمارش اشیای NetworkInterface به دنبال یک قرارداد است، نه یک واقعیت.
هر دو جهت خطا زنده است. دستگاه میتواند برای تونلی محلی که هیچ ارتباطی با VPN کاربر ندارد tun0 داشته باشد، و یک تونل WireGuard کاملاً سالم میتواند برای بررسیای که فقط رشته tun0 را میشناسد نامرئی باشد. مثبتهای کاذب و منفیهای کاذب در یک تابع آن را بهعنوان ابزار تشخیص بیفایده میکند، زیرا تیکت پشتیبانیای که میگوید «VPN یافت نشد» در هیچ جهتی قابل اعتماد نیست.
دسترسی هم محدودتر شده است. نسخههای جدیدتر اندروید محدود میکنند که یک برنامه معمولی چه چیزی از /proc بخواند، بنابراین بررسی مبتنی بر نام که در اندروید 8 کار میکرد میتواند در دستگاهی جدیدتر بدون هیچ تغییری در کد فهرست خالی برگرداند. وقتی بررسی بیصدا در جهت باز شکست میخورد، برنامه نتیجه میگیرد که روی سختافزاری که تونل در آن سالم است VPN وجود ندارد.
افسانه: ping موفق یعنی تونل ترافیک را حمل میکند
ping به میزبان داخلی هرگاه مسیری به آن میزبان وجود داشته باشد موفق میشود. با تونل تقسیمشده، میزبان ممکن است از طریق Wi-Fi در دسترس باشد در حالی که تونل فعال است و کار کاملاً دیگری میکند. با VPN بهازای هر برنامه، برنامهای که ping میکند ممکن است از تونل مستثنی باشد، پس پژواک ICMP بیرون از آن خارج میشود و برمیگردد، که خلاف آنچه بررسی فرض میکند را ثابت میکند.
ICMP و TCP هم جداگانه فیلتر میشوند. تونلی که درخواستهای پژواک را مجاز میکند اما درگاه TCP مورد نیاز یک سرویس را میاندازد، آزمون ping را میگذراند و در برنامه شکست میخورد، و این قاعده دیوار آتش در طرف مقابل است نه ویژگی دستگاه. تنها خوانش صادقانه یک ping موفق این است که این مقصد در این لحظه به این پروتکل پاسخ داده است.
پلتفرم واقعاً چه چیزی گزارش میکند
از API 21 به بعد، ConnectivityManager.getAllNetworks() هر شبکهای را که سیستم میشناسد برمیگرداند و getNetworkCapabilities(network) گزارش میکند که آن شبکه چه اعلام میکند. شبکه VPN حامل TRANSPORT_VPN است. گردش روی همه شبکهها به یک پرسش پاسخ میدهد: آیا اکنون هیچ VPN روی این دستگاه فعال است؟
پرسش دوم و محدودتر به فراخوانی دیگری نیاز دارد. getActiveNetwork() شبکهای را برمیگرداند که ترافیک برنامه اکنون از آن استفاده میکرد، و بررسی همان قابلیت روی آن پاسخ میدهد: آیا ترافیک من از VPN عبور میکند؟ این دو میتوانند ناسازگار باشند و این ناسازگاری آموزنده است. VPNای که فعال است اما جاری نیست به مسیریابی بهازای هر برنامه یا به پیکربندی تونل تقسیمشده اشاره میکند، نه به تونل گمشده.
Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean viaVpn = caps != null
&& caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
هر دو فراخوانی را محافظت کنید. قابلیتها میتوانند برای شبکهای که بین فهرستکردن و پرسیدن ناپدید شده null باشند، و شبکه فعال میتواند در حین جابهجایی null باشد. NET_CAPABILITY_NOT_VPN در جهت دیگر سودمند است، تا تأیید کنید شبکهای قطعاً تونل نیست، نه اینکه فقط پرچم ترانسپورت VPN را ندارد.
NetworkInfo همان افسانه با یک API بود
ConnectivityManager.TYPE_VPN همراه با NetworkInfo نسل دوم این بررسی است و به دلیلی منسوخ شده است. در نسخههای مدرن شیء برگشتی برای VPN میتواند null باشد در حالی که تونل متصل است، بنابراین کدی که null را «بدون VPN» میداند بیصدا نتیجه منفی کاذب را روی دستگاههایی گزارش میکند که پاسخ در آنها بیشترین اهمیت را دارد.
پخشی که معمولاً همراهش است هم بهتر نیست. CONNECTIVITY_CHANGE سابقه مستندی از از دست دادن گذارهای VPN دارد، از جمله نسخههایی که فقط هنگام اتصال یا قطع VPN شلیک میشد نه در هر تغییر شبکه. شنوندهای که بر آن ساخته شود متغیر حالتی تولید میکند که تا اتصال کامل بعدی از دید پلتفرم فاصله میگیرد.
به callbackها واکنش دهید نه به نظرسنجی
یک NetworkRequest ثبت کنید که TRANSPORT_VPN را الزامی میداند و NET_CAPABILITY_NOT_VPN را حذف میکند، سپس registerNetworkCallback را با آن درخواست فراخوانی کنید. آنگاه onAvailable و onLost به شما میگویند اعلام چه زمانی تغییر میکند، و این همان رویدادی است که واقعاً برایتان مهم است. وقتی مؤلفه متوقف میشود ثبت callback را لغو کنید، وگرنه برنامه ارجاعی به صفحهای مرده نگه میدارد و callback همچنان شلیک میشود.
NetworkRequest req = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();
cm.registerNetworkCallback(req, callback);
این به ACCESS_NETWORK_STATE نیاز دارد که مجوزی عادی است: بدون root، بدون گفتوگوی رضایت کاربر، بدون مجوز VPN. نظرسنجی با تایمر جایگزینی است که مردم به آن پناه میبرند، و باتری را میسوزاند و در همان حال گذاری کوتاهتر از فاصله را از دست میدهد، که دقیقاً همان گذاری است که کاربر گزارش میکند.
آنچه پاسخ هنوز نمیتواند به شما بگوید
هیچیک از این APIها گزارش نمیکند که تونل سالم است یا نه، روی کدام سرور پایان مییابد، یا ترافیک برنامهای خاص داخل آن است یا نه. شبکه VPN میتواند بدون مسیرهای قابل استفاده وجود داشته باشد، و تونل بهازای هر برنامه میتواند برنامهای را که پرسش را میپرسد مستثنی کند.
پس آنچه بررسی دیده را ثبت کنید، نه فقط مقدار بولی را. شناسه شبکه، ترانسپورتهای اعلامشده و اینکه آن شبکه فعال بود را ثبت کنید. گزارشی که میگوید «VPN یافت نشد» بیفایده است، در حالی که گزارشی که میگوید شبکهای TRANSPORT_VPN اعلام کرده اما getActiveNetwork() مقدار Wi-Fi برگردانده، مستقیماً به مسیریابی یا فهرست استثنا اشاره میکند، و این اصلاحی است که کسی میتواند روی آن کار کند.