چرا تونل VpnService اندروید شما مسیریابی ترافیک را متوقف میکند
چهار باور درباره VpnService که در محیط تولید میشکنند: هر بار یک تونل، دامنهٔ مسیرها، فهرستهای هر برنامه و ترتیب خاموشسازی، با یک بررسی برای هر کدام.
چهار فرض درباره Android VpnService یک بار بیشتر یک هفتهام را گرفت، و هیچکدام درباره پروتکل شبکه نبود. برنامه میگفت متصل است، نماد کلید در نوار وضعیت نشسته بود، و ترافیک هنوز از مسیر عادی بیرون میرفت. من مدام فرض میکردم که تونل تا وقتی خودم متوقفش نکنم مال من است، که مسیرهایی که اضافه میکنم همان مسیرهایی هستند که میگیرم، و که فراخوانی stopSelf رابط را با خودش پایین میآورد. سرویس دقیقاً همانطور که مستند شده رفتار میکرد. بخش خراب مدل ذهنی من بود.
افسانه 1: تونل تا وقتی خودتان متوقفش نکنید برپا میماند
اندروید هر بار یک رابط VPN نگه میدارد. دومی را برپا کنید و پلتفرم آن را در صف نمیگذارد، بلکه رابط شما را غیرفعال میکند و onRevoke() را روی سرویس شما صدا میزند. اگر کد شما فقط با فشار یک دکمه پایین بیاید، یک تصاحب پشت سر خود یک توصیفگر و یک رشتهٔ خواندن جا میگذارد، و شروع بعدی هر دو را به ارث میبرد.
مرجع VpnService صریحاً میگوید که prepare() وقتی برنامه از قبل رضایت دارد null برمیگرداند، و یک Intent وقتی کاربر هنوز باید تأیید کند. این رضایت فقط وقتی میرسد که قصد را از طریق startActivityForResult() تحویل دهید. تحویل را رد کنید و establish() null برمیگرداند، که بهعنوان یک شکست خاموش خوانده میشود نه یک مشکل مجوز.
Always-on VPN مالکی اضافه میکند که شما کنترلش نمیکنید. سیستم میتواند تونل را پس از بوت یا پس از یک خرابی برگرداند، و از یک برنامهٔ VPN که کاربر دستی شروع کرده پیشی میگیرد. onRevoke() را چنان بسازید که همان منابعی را ببندد که خاموشسازی عادی شما میبندد، و آن را یک مسیر توقف اصلی بدانید نه یک حالت حاشیهای. کاربران برنامههای VPN را عوض میکنند؛ کد شما باید از این جان سالم به در ببرد.
افسانه 2: addRoute دقیقاً همان چیزی را میگیرد که فهرست کردهاید
مسیرها تعیین میکنند کدام بستهها به توصیفگر شما برسند، و یک خطای دامنه آن حالت دیوانهکننده را میسازد که بخشی از ترافیک از تونل میرود و بخشی نمیرود. addRoute("0.0.0.0", 0) همهٔ IPv4 را میگیرد، یعنی هر بسته اکنون به شما میرسد و باید فرستاده یا دور انداخته شود. یک /32 برای یک میزبان تنها تقریباً هیچ چیز نمیگیرد، و بقیه چیزها طوری رفتار میکنند که گویی VPN شما هرگز نصب نشده است.
خانوادهها جداگانه شمرده میشوند. یک مسیر فقط IPv4، پروتکل IPv6 را روی شبکهٔ زیرین باقی میگذارد، پس یک میزبان دوپشته هنوز میتواند بیرون تونل نام را حل کند و وصل شود در حالی که داخل آن کاملاً متصل به نظر میرسد.
DNS از همان منطق پیروی میکند. سرورهایی که با addDnsServer() اضافه شدهاند به برنامههایی میروند که از حلکنندهٔ تونل استفاده میکنند، در حالی که برنامهای با نقطهٔ پایانی DNS رمزنگاریشدهٔ خودش یا Private DNS تنظیمشده روی یک نام میزبان، آنها را کاملاً نادیده میگیرد. وقتی نامها برای یک برنامه شکست میخورند و برای دیگری کار میکنند، مسیر حلکننده را بررسی کنید پیش از آن که حتی یک مسیر را لمس کنید.
افسانه 3: قواعد هر برنامه کلیدی است که میتوانید به هر دو طرف بچرخانید
addDisallowedApplication() و addAllowedApplication() دو حالتاند، نه دو فیلتر. سازنده برای هر تونل یک مدل میپذیرد، پس برنامهای که همه چیز جز سه بسته را میخواهد از فهرست ممنوع استفاده میکند و فهرست مجاز را خالی میگذارد. این دو یکدیگر را رد میکنند، و مردم این را در زمان اجرا کشف میکنند بعد از آن که صفحهٔ تنظیمات از قبل هر دو کلید را پیشنهاد داده است.
دیدپذیری بستهها هم گاز میگیرد. در Android 11 و بعد از آن فقط میتوانید بستههایی را نام ببرید که میبینید، پس فهرست استثنایی که از پرسشی ساخته شده که برنامهٔ شما اجازهٔ اجرای آن را ندارد، بیصدا ورودیها را از دست میدهد. فهرست را با آنچه مدیر بسته واقعاً برمیگرداند مقایسه کنید پیش از اعتماد به آن.
نمایههای کاری مخاطب را دوباره تقسیم میکنند. تونلی که داخل نمایه شروع میشود آن نمایه و برنامههایش را پوشش میدهد، در حالی که سمت شخصی اتصال خودش را نگه میدارد. این یک تصمیم دامنه است که یک بار گرفته میشود، و جای آن در رابط کاربری است نه در یک تیکت پشتیبانی.
افسانه 4: stopSelf تونل را تمام میکند
تمام نمیکند. رابط در توصیفگری زندگی میکند که establish() برمیگرداند، و شیء سرویس میتواند بمیرد در حالی که آن توصیفگر باز میماند. ابتدا ParcelFileDescriptor را ببندید، ارجاع را رها کنید، رشتههای خواندن و نوشتن را به هم برسانید یا قطع کنید، سپس stopForeground(true) و stopSelf() را به همین ترتیب صدا بزنید.
توصیفگر فراموششده دلیل معمول این است که به نظر میرسد یک سرویس از درخواست توقف خودش جان سالم به در میبرد، و مسیر کهنهای را توضیح میدهد که بیشتر از برنامهای که آن را اضافه کرده زنده میماند.
خطای معکوس هم به همان اندازه رایج است: بستن توصیفگر در حالی که یک رشتهٔ خواندن در میانهٔ خواندن است، و سپس رها کردن آن رشته برای رسیدگی به استثنایی که کسی برایش کد ننوشته است. توالی خاموشسازی را در یک جا نگه دارید و بگذارید هم اقدام کاربر و هم onRevoke() به آن فراخوانی کنند.
وقتی تونل مرده به نظر میرسد چه چیزی را بررسی کنید
سوکتهای دیرزیست را با protect() پیش از بالا آمدن تونل محافظت کنید. بدون آن، خودِ ترابری تونل دوباره به داخل تونل مسیریابی میشود و اتصال بهشکلی گیر میکند که دقیقاً شبیه خطای سرور است. در برابر آدرسی آزمایش کنید که فقط پشت شبکهٔ دور وجود دارد، نه یک سایت عمومی که از حافظهٔ نهان یا از مسیر زیرین پاسخ میدهد.
نتیجهٔ establish()، مسیرها، بستههای استثناشده و دلیل خاموشسازی را ثبت کنید. آن چهار خط مشکل رضایت را از مشکل دامنه و از مشکل چرخهٔ عمر بدون گرفتن بسته جدا میکند.