Android VPN: توصیفگر VpnService در برابر دستگاه tun روت
برنامههای اندروید نمیتوانند خودشان tun0 بسازند: establish() چه برمیگرداند، Binder کجا میایستد و JNI کجا آغاز میشود، و چه وقت دستگاه ساختهشده با root برنده میشود.
دو مسیر میتوانند یک رابط IP را جلوی یک برنامهٔ اندروید بگذارند و هر دو به همان نوع توصیفگر فایل با مالکان بسیار متفاوت میرسند. یکی از VpnService و سازندهٔ آن میگذرد و دیگری از یک پوستهٔ root و /dev/net/tun. هر خواندن روی آن توصیفگر یک بستهٔ خروجی است و هر نوشتن یک بستهٔ ورودی. آنچه این مسیرها را جدا میکند این است که سیاست در دست کیست، کدامیک از ارتقای سیستمعامل جان سالم به در میبرد و چقدر از جدول مسیریابی را میتوانید لمس کنید.
مسیر پلتفرم را انتخاب کنید مگر اینکه بتوانید آن چیز مشخصی را که از انجامش سر باز میزند نام ببرید. آن امتناع معمولاً TAP است، یا ضبط خارج از مجموعهٔ برنامههای خودتان، یا قاعدهٔ مسیریابیای که چارچوب هرگز آشکار نمیکند.
در مسیر پلتفرم شما قصد را اعلام میکنید نه دستورها را. حداقل یک نشانی، مسیرهای مورد نظر، سرورهای DNS و فهرست اختیاری برنامهها را میدهید، سپس سرویس سیستم اندروید راهاندازی ممتاز را با هویت خودش انجام میدهد. هرگز نام رابط را از پیش نمیآموزید و هرگز ioctl ای را که آن را میسازد صادر نمیکنید، و همین نکتهٔ اصلی است.
دو راه برای گرفتن یک رابط IP
مسیر root دستور قدیمیتر است. /dev/net/tun را باز میکنید، TUNSETIFF را با IFF_TUN صدا میزنید، رابط را با ip link بالا میآورید و نشانیها و مسیرها را دستی اضافه میکنید. هر گام را باید خودتان درست انجام دهید، از جمله گامهایی که چارچوب بیصدا انجام میداد. یک مسیر جاافتاده همان گزارش کاربر را میدهد که یک سرور مرده.
اشکالزدایی به همان اندازه پیکربندی متفاوت است. در مسیر root یا رابط وجود دارد یا ioctl شما خطایی برگردانده که میتوانید مستقیم بخوانید. در مسیر پلتفرم رابط فقط پس از موفقیت establish() وجود دارد، بنابراین شکست بهصورت استثنا یا توصیفگر null میرسد نه بهصورت گره دستگاه که بتوانید در آن کندوکاو کنید.
establish() واقعاً چه برمیگرداند
Builder.establish() سه پیامد دارد و هر سه پاسخ متفاوتی میخواهند. توصیفگر یعنی رابط پیکربندی شده و برای استفاده در اختیار شماست، null یعنی درخواست عبور نکرده، و استثنای پرتابشده یعنی سرویس سیستم آن را رد کرده است. یکیکردن null و موفقیت در یک boolean رایجترین راهی است که یک برنامه از تونلی کارآمد خبر میدهد که هرگز بستهای حمل نمیکند. مرجع Builder شرط نشانی را بیان میکند و ارزش خواندن دارد پیش از آنکه سرور را سرزنش کنید.
جهتها را توصیفگر تعیین میکند نه پروتکل شما. خواندنها بستههای خروجی را که پیشتر برای VPN انتخاب شدهاند به شما میدهند و نوشتنها بستههای ورودی را به رابط برمیگردانند. لایهٔ انتقال خودتان این بستهها را رمزگذاری، احراز و قاببندی میکند، چون پلتفرم در مرز بسته متوقف میشود.
Binder یک گام است، JNI محوری دیگر
درخواست سازنده از Binder به سیستم میرسد و جایی پایینتر پلتفرم کد بومی را برای پیکربندی دستگاه فرا میخواند. اینها دو سازوکار متفاوتاند که در یک فراخوان انباشته شدهاند. Binder ارتباط بینفرایندی میان برنامهٔ شما و یک سرویس سیستم است، در حالی که JNI پل Java به کد بومی درون یک فرایند است. خواندن درخت AOSP اندروید 9 پشته را تحتاللفظی نشان میدهد و خوب است به یاد داشته باشید نام کلاسهای درونی و مسیرهای فایل بین نسخهها جابهجا میشوند.
آنچه جابهجا نمیشود قراردادی است که میتوانید به آن تکیه کنید: سازنده، توصیفگری که برمیگرداند و چرخهٔ عمر سرویس پیرامونش. هیچ برنامهای نباید برای گرفتن رابط، تراکنش درونی سرویس را صدا بزند یا گره دستگاه را از نو بسازد. اگر کد شما به نام کلاسی از AOSP وابسته باشد، روی نسخهای که آزمایش نکردهاید میشکند.
TUN بستههای IP را حمل میکند، TAP اترنت را
TUN یک رابط IP است. TAP یک رابط اترنت است و همان تفاوت تعیین میکند که پلسازی اصلاً ممکن است یا نه. دستگاه TUN نه نشانی MAC دارد و نه سرآیند اترنتی برای ارسال، بنابراین کلاینت TUN نمیتواند به پل لایهٔ 2 بپیوندد، هر طور که مسیرها نوشته شده باشند.
VpnService گونهٔ IP را به شما میدهد که همان چیزی است که VPN سطح برنامه لازم دارد و میتواند بدون پشتهٔ خام اترنت از آن پشتیبانی کند. اگر طراحی شما پلسازی میخواهد، مسیر پلتفرم شما را به آنجا نمیبرد و مسیر root تنها وقتی میبرد که هسته و مجوزهای دستگاه همکاری کنند.
کجا مسیر root هنوز برنده است
سه کار مردم را از مسیر پلتفرم دور میکند. پلسازی لایهٔ 2 به TAP نیاز دارد. ضبط ترافیک برنامههای خارج از گسترهٔ VPN شما به دیدپذیریای نیاز دارد که چارچوب نمیدهد. مسیریابی سیاستی که سازنده هرگز آشکار نمیکند، مانند جدولی جدا برای یک مقصد، به قواعدی نیاز دارد که خودتان نصب میکنید.
هزینهها بعدتر میرسند: بوتلودر باز یا ایمیج روتشده، سیاست SELinux که زیر پای شما جابهجا میشود و دستگاهی که پس از یک OTA دیگر درست رفتار نمیکند. دستور روتشده را هم نمیتوان از توزیع عادی برنامهها منتشر کرد، بنابراین بیشتر تیمها آن را بهجای محصول، مسیری داخلی برای عیبیابی نگه میدارند.
انتخاب میان آن دو
از رابطی که لازم دارید شروع کنید نه از کدی که دارید. اگر پاسخ دستگاهی TUN با مسیرهای محدود به برنامه است، مسیر پلتفرم کوتاهتر است و از ارتقاها جان سالم به در میبرد. اگر پاسخ شامل قابهای اترنت، ضبط برنامههای دیگر یا قواعد مسیریابیای است که سازنده رد میکند، در قلمرو root هستید و باید آن را بهعنوان هزینهٔ عملیاتی در بودجه بگنجانید.
بررسیای که بیشتر بحثها را حل میکند: نخست مسیر پلتفرم را بسازید، نتیجهٔ establish() را جدا از وضعیت نشست دور ثبت کنید و یک بسته در هر جهت را تأیید کنید. تنها پس از آن تصمیم بگیرید که آیا توانایی غایب، ترک مسیر پشتیبانیشده را توجیه میکند.