برنامه شما VpnService.prepare را فرا می‌خواند، null می‌گیرد، تونل می‌سازد و توصیفگر معتبر به نظر می‌رسد، اما هیچ چیز به سرور نمی‌رسد. یا prepare یک Intent برمی‌گرداند، کاربر آن را تأیید می‌کند و establish باز هم شکست می‌خورد. هر دو نتیجه معمولاً یک معنا دارند: برنامه دیگری از قبل مالک رابط VPN این کاربر است و فراخوانی‌های protect شما سوکت‌هایی را علامت می‌زنند که تونل شما نمی‌تواند استفاده کند. بررسی‌های زیر را به ترتیب انجام دهید و در اولین بررسی ناموفق توقف کنید، زیرا یک مرحله بعدی نمی‌تواند مرحله قبلی را ترمیم کند.

قبل از دست زدن به protect، جایگاه VPN را بررسی کنید

اندروید به هر کاربر دقیقاً یک رابط VPN فعال می‌دهد. این قاعده پلتفرم است، نه اجازه‌ای که برنامه شما بتواند درخواست کند، پس دو کلاینتی که هر دو می‌خواهند تونل باشند نمی‌توانند با هم وجود داشته باشند.

VPN همیشه‌فعالی که برای برنامه دیگری تنظیم شده سخت‌ترین حالت است، زیرا سیستم آن برنامه را متصل نگه می‌دارد و تصاحب را رد می‌کند. یک فیلتر ترافیک مانند AdGuard، یک کلاینت شرکتی یا یک VPN مصرفی می‌تواند جایگاه را نگه دارد در حالی که کاربر فراموش کرده نصب است. از کاربر بخواهید تنظیمات را باز کند و ورودی فهرست‌شده زیر VPN را بخواند؛ نامی که آنجاست همان مالکی است که با او رقابت می‌کنید. تا زمانی که آن ورودی قطع نشود یا تنظیم همیشه‌فعال پاک نشود، VpnService شما نمی‌تواند رابط شود.

مرحله 1: بخوانید prepare چه برمی‌گرداند

VpnService.prepare به یک پرسش پاسخ می‌دهد: آیا این برنامه می‌تواند VPN شود؟ وقتی رضایت از قبل وجود دارد null برمی‌گرداند و وقتی کاربر هنوز باید آن را تأیید کند Intent می‌دهد که باید با startActivityForResult اجرا کنید. بازگشت null را اجازه ادامه بدانید، نه سندی بر اینکه establish موفق می‌شود.

Intent intent = VpnService.prepare(context);
if (intent != null) {
    startActivityForResult(intent, REQUEST_VPN);
}

اگر مسیر Intent برگردد و کاربر رد کرده باشد، prepare در تلاش بعدی باز هم Intent برمی‌گرداند. کد نتیجه را ثبت کنید به جای تلاش دوباره در حلقه، زیرا رضایت ردشده تصمیم کاربر است، نه خطای گذرا.

مرحله 2: تأیید کنید که رابط متعلق به برنامه شماست

پس از establish، ParcelFileDescriptor که دریافت می‌کنید سند مالکیت شماست و ارزش دارد پیش از هر کار سوکت آن را بررسی کنید. توصیفگری که null یا از قبل بسته است یعنی رابط هرگز ساخته نشده، حتی اگر هیچ استثنایی ظاهر نشود. نام رابط را از سازنده با نامی که سیستم برای VPN فعال گزارش می‌کند مقایسه کنید، با استفاده از dumpsys روی یک دستگاه آزمایشی. اگر متفاوت باشند، تونلی که ساخته‌اید همان تونلی نیست که ترافیک را حمل می‌کند.

adb shell dumpsys connectivity

همین ناسازگاری توضیح می‌دهد چرا یک آیکون متصل با ترافیک مرده دیده می‌شود. سیستم تونل برنامه دیگر را نشان می‌دهد، در حالی که کد شما توصیفگر رابطی را دارد که مالک آن نیست.

مرحله 3: پیش از connect، protect را فراخوانی کنید

هنگامی که رابط وجود دارد، سوکت‌هایی که برنامه شما باز می‌کند به‌طور پیش‌فرض به تونل هدایت می‌شوند. ترابری بیرونی نمی‌تواند از این قاعده پیروی کند وگرنه بسته کپسوله‌شده به رابط بازمی‌گشت، بنابراین VpnService protect یک سوکت را علامت می‌زند تا VPN را دور بزند و از شبکه زیرین استفاده کند. فراخوانی یک boolean برمی‌گرداند و نادیده گرفتن آن همان راهی است که به سوکتی می‌رسید که سالم به نظر می‌رسد و چیزی حمل نمی‌کند.

protect را پیش از connect یا bind فراخوانی کنید، هرگز بعد از آن. محافظت باید پیش از اتصال یا مقید شدن سوکت برقرار باشد، زیرا سوکت متصل مسیر خود را انتخاب کرده و protect بعدی آن را به‌طور قابل اعتماد جابه‌جا نمی‌کند. اگر protect مقدار false برگرداند، توقف کنید و مالکیت را دوباره بررسی کنید: زمانی شکست می‌خورد که رابط متعلق به برنامه شما نباشد، و این دقیقاً همان حالتی است که VPN دیگری باقی می‌گذارد. بازگشت false نشانه‌ای برای بازگشت به مرحله 1 است، نه تلاش دوباره همان سوکت.

DatagramSocket socket = new DatagramSocket();
if (!vpnService.protect(socket)) {
    return;
}
socket.connect(serverAddress, serverPort);

مرحله 4: وقتی سوکت دشوار است یک فرایند را مقید کنید

برخی ترابری‌ها یک سوکت نیستند: یک کتابخانه ممکن است چندین سوکت باز کند، یا می‌خواهید هر اتصال در یک فرایند کمکی از همان خروجی استفاده کند. ConnectivityManager.bindProcessToNetwork فرایند را به شبکه انتخاب‌شده مقید می‌کند و setProcessDefaultNetwork قدیمی‌تر پیش از منسوخ شدن همان کار را می‌کرد. اتصال زیرین را با NET_CAPABILITY_NOT_VPN درخواست کنید تا مقیدسازی شبکه زیر تونل شما را انتخاب کند، نه خود تونل را.

NetworkRequest request = new NetworkRequest.Builder()
    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
    .build();

مقید کردن یک فرایند VPN دوم نیست. برای آن فرایند یک خروجی انتخاب می‌کند، که برای ترابری شما سودمند است و برای اجرای دو تونل همزمان بی‌فایده.

دستگاه کاربر را درست کنید، نه مسیر کد را

هیچ API‌ای وجود ندارد که به دو برنامه اجازه دهد جایگاه VPN را شریک شوند، پس راه‌حل به وضعیت دستگاه مربوط است. دو مسیر مشخص به کاربر پیشنهاد دهید: VPN برنامه دیگر را قطع کنید یا تنظیم همیشه‌فعال آن را پاک کنید تا prepare شما بتواند تصاحب کند، یا تنظیمات هر برنامه آن را باز کنید و برنامه خود را از تونل آن مستثنی کنید. استثنا فیلتر را برای همه چیزهای دیگر فعال نگه می‌دارد و اجازه می‌دهد برنامه شما از اتصال معمولی استفاده کند، که اغلب همان نتیجه‌ای است که کاربر واقعاً می‌خواهد.

هر دو مسیر را در مستندات پشتیبانی بنویسید، زیرا کاربران به‌ندرت می‌دانند کدام برنامه مالک VPN است. از آنها بخواهید نام VPN فعال را از تنظیمات و نتایج prepare، establish و protect را از گزارش‌های شما بفرستند، و در یک دقیقه می‌توانید بگویید کدام مرحله شکست خورده است. سپس با ترافیک واقعی تأیید کنید: یک جست‌وجوی نام، یک اتصال TCP و یک درخواست از طریق تونل، نه فقط یک آیکون متصل. یک توصیفگر معتبر و یک نشانگر سبز حداکثر مالکیت را ثابت می‌کنند، هرگز نه اینکه ترابری از قواعد مسیریابی جان سالم به در برده باشد.

اگر هر چهار بررسی موفق شوند و ترافیک هنوز می‌میرد، مالکیت دیگر مشکل شما نیست. به مسیریابی و دسترس‌پذیری سرور بروید و همان اعتبارنامه‌ها را از دستگاهی بدون برنامه VPN نصب‌شده آزمایش کنید.

مقالات مرتبط