VpnService اندروید وصل میشود اما هیچ بستهای به شبکه نمیرسد
تونل بالا آمده و صفحهها هنوز شکست میخورند: پیش از متهم کردن سرور VPN، حالت خواندن، محافظت سوکت، پشتیبانی TCP و فهرست مسیرها را بررسی کنید.
تونل بالا میآید، نماد کلید پدیدار میشود و هر درخواست مرورگر همچنان به پایان مهلت میرسد. در یک برنامه VpnService این ترکیب معمولاً یعنی حلقه خواندن زنده است و نیمه خروجی خراب، نه اینکه سرور دستدادن را رد کرده باشد. اندروید خط لوله را ساخته، ترافیک برنامه را به آن منتقل کرده و به فرایند شما یک توصیفگر فایل داده است. هر چیزی بعد از آن توصیفگر کد شماست و یک فراخوانی جاافتاده کافی است تا کل جریان بلعیده شود.
چیزی که سیستم میدهد و چیزی که نمیدهد
تابع establish() یک ParcelFileDescriptor برای خط tun برمیگرداند. سیستم بستههای IP خروجی را در آن مینویسد و انتظار دارد فرایند شما آنها را بخواند، تصمیم بگیرد کجا میروند و از طریق یک سوکت معمولی بفرستد. زیر آن هیچ ارسال هستهای وجود ندارد، پس وقتی کسی نمیخواند بستهها دور ریخته میشوند و شمارندههای خط هرگز تکان نمیخورند.
به جای حدس زدن، بشمارید. در هر خواندن موفق یک شمارنده را افزایش دهید و مقصد و پروتکل چند بسته نخست را ثبت کنید. اگر شمارنده صفر بماند در حالی که ترافیک تولید میشود، خطا بالاتر از کد ارسال شماست و جستوجو را به پیکربندی builder و حالت خواندن محدود میکند. اگر شمارنده حرکت کند و مرورگر باز هم شکست بخورد، خطا در مسیر ارسال یا مسیر بازگشت است.
خواندن مسدودکننده تعیین میکند که حلقه حرکت کند یا نه
setBlocking(true) توصیفگر را در حالت مسدودکننده میگذارد و از API 21 وجود دارد. در نسخههای قدیمی راهحل یک دستیار کوچک JNI است که O_NONBLOCK را با fcntl روی توصیفگر خام پاک میکند. حالت اهمیت دارد چون تعیین میکند خواندن بدون بسته در انتظار چه کاری مجاز است.
در حالت نامسدود آن خواندن فوراً با مقدار منفی برمیگردد و حلقهای که نتیجه را نادیده میگیرد یک هسته را میچرخاند بیآنکه هیچ ترافیکی جابهجا کند. در حالت مسدود خواندن منتظر میماند، که همان چیزی است که رشته ارسال میخواهد، اما آنگاه دیگر نمیتوان رشته را قطع کرد. بستن توصیفگر روش پشتیبانیشده برای واداشتن خواندن به شکست است تا حلقه بیرون برود. کشتن فرایند یا قطع رشته میتواند شروع بعدی را با توصیفگری رها کند که دیگر با خط سازگار نیست.
در هر فراخوانی یک بسته در بافری به اندازه MTU خط بخوانید و صفر یا بازگشت منفی را خطا بدانید نه بسته خالی.
protect() پیش از نخستین ارسال، نه پس از آن
هر سوکتی که بسته کپسولهشده حمل میکند باید نخست محافظت شود. سوکت محافظتنشده همان قواعد مسیریابی را که همین حالا نصب کردهاید رعایت میکند، پس بستههایش به تونل بازمیگردند و حلقه خواندن خودتان آنها را دوباره برمیدارد. سپس تونل خروجی خودش را بیپایان ارسال میکند و از بیرون مانند اتصالی به نظر میرسد که باز میشود و هرگز پاسخ نمیدهد.
ترتیب چنین است: protect، سپس connect یا bind، سپس send. فراخوانی protect بعد از connect میتواند تصمیم مسیر پیشین را سر جایش بگذارد، پس سوکت همچنان مسیر نادرست را به کار میبرد حتی اگر فراخوانی true برگردانده باشد. مقدار بولی برگشتی را بررسی کنید و وقتی false است بلند شکست بخورید، به جای آنکه به ارسالی بروید که نمیتواند کار کند.
خطای مجوز از sendto روی سوکتی که هرگز protect() را لمس نکرده، امضای معمول این اشتباه است.
نمونه UDP ترافیک HTTPS را حمل نمیکند
نمونه ارسالی که بیشتر آموزشها کپی میکنند فقط UDP را پوشش میدهد. TCP به دستدادن، شمارههای ترتیب، تأییدها و ارسال دوباره نیاز دارد و حلقهای که بار داده را میان توصیفگر tun و یک سوکت کپی میکند هیچیک از اینها را فراهم نمیکند. به همین دلیل یک پرسوجوی DNS روی UDP عبور میکند در حالی که هر درخواست HTTPS گیر میکند. یا پشته TCP را در فضای کاربر اجرا کنید یا اتصالها را به پروکسی محلی بسپارید که TCP را روی دستگاه پایان میدهد و جریان را ارسال میکند.
پیش از بازنویسی هر چیزی، تقسیم را بیازمایید: یک پرسوجوی UDP بفرستید و یک اتصال TCP به همان میزبان باز کنید. اگر مسیر UDP کار کند و مسیر TCP روی نخستین SYN بماند، مشکل کد ارسال است نه سرور. همین یک مقایسه زمان زیادی را صرفهجویی میکند که صرف خواندن گزارشهای سرور میشود که چیزی سودمند ندارند.
فهرست مسیرها و فیلترهای هر برنامه تعیین میکنند چه میبینید
addAddress، addRoute و addDns تعیین میکنند سیستم چه چیزی به تونل میفرستد. مقصدی بیرون از فهرست مسیرها هرگز به توصیفگر شما نمیرسد، پس هیچ حجمی از منطق ارسال آن را درست نمیکند. فراخوانیهای builder را ثبت کنید و با نشانیهایی مقایسه کنید که برنامه واقعاً میکوشد به آنها برسد.
انتخاب بر اساس برنامه کف خودش را دارد: addAllowedApplication و addDisallowedApplication به API 21 نیاز دارند و در نسخههای قدیمی این متدها بهسادگی وجود ندارند. طرحی که بر پایه بسته فیلتر میکند هرگز آنجا اجرا نمیشود.
برقراری دوباره توصیفگرهای کهنه را جا میگذارد
فراخوانی دوباره establish() توصیفگر تازه برمیگرداند و خط پیشین را میبندد. توصیفگر قدیمی اکنون نامعتبر است، پس رشتهای که هنوز روی آن مسدود است خطا میگیرد و سوکتهای ساختهشده برای مسیر قدیم ممکن است باید جایگزین شوند. onRevoke فراخوانی بازگشتی برای تونلی است که کاربر یا سیستم گرفته است. نگهداشتن چرخه عمر نزد یک مالک، که پیش از برقراری دوباره خواننده را متوقف میکند، بیشتر حالتهای وصلشده-اما-مرده را جلوگیری میکند.
هر خطا را بر پایه گامی که به آن تعلق دارد بخوانید. بازنشانی اتصال از recvfrom یعنی طرف دور بست در حالی که کد شما هنوز میخواند. خطای نشانی بد به تجزیه سرآیند یا اشکال جابهجایی اشاره دارد نه به شبکه.
ترتیب کار: حالت مسدود روشن، یک بسته در هر خواندن، سرآیند تجزیهشده، سوکت محافظتشده، بار ارسالشده، بستن پاک در تحویل و فهرست مسیرهایی که مقصد را پوشش میدهد.