Список дозволених в Android VpnService: чому дозволені сторінки досі зависають
Ваш список дозволених, найімовірніше, гаразд. Сторінки зависають через відповідь DNS, таблицю маршрутів, сокет резолвера або MTU. Ось порядок перевірки.
Ви додали домен до списку дозволених, запит доходить до застосунку, зіставляювач повертає дозволено, а сторінка так і не завантажується. У цей момент більшість починає переписувати зіставляювач і додавати порівняння з шаблонами. За моїм досвідом зіставляювач зазвичай правильний, а збій лежить на рівень нижче: у відповіді DNS, яку ви синтезували, у маршруті, який ви встановили, або в сокеті, яким пересилали запит. Список — це проста частина; ламаються списки дозволених саме на шляху пакета.
Зіставляювач рідко буває винним
Спочатку треба розділити заблоковане і зламане. Заблоковане ім'я має відмовляти швидко й помітно, бо браузер отримує відповідь, що імені немає, і зупиняється. Зламане ім'я зависає, повторює спроби через IPv6, переходить на інший транспорт, і ви залишаєтесь із крутним індикатором. Якщо ви бачите крутний індикатор, ваш список дозволених, найімовірніше, відповідає чимось, із чим клієнт не може нічого зробити.
Порожній запис A — класична помилка. Відповідь NOERROR з нулем записів виглядає як припустима, тому stub-резолвер її приймає, браузер пробує знову, пробує AAAA, пробує інший резолвер і здається значно пізніше. Код відповіді 3, тобто NXDOMAIN, із повтореною секцією питання і скопійованим ідентифікатором транзакції завершує розмову негайно. Використовуйте його для кожного імені, яке ви навмисно відхиляєте.
IPv6 теж заслуговує на те саме рішення, а не на значення за замовчуванням. Якщо ви відповідаєте NXDOMAIN на A і пропускаєте AAAA, клієнт може дістатися заблокованого імені через IPv6, і фільтр протікає. Деякі реалізації відповідають на заблоковані запити AAAA адресою loopback, щоб з'єднання падало одразу, а не переходило на запасний шлях. Виберіть одну політику, застосуйте її до обох типів записів і журналюйте обидва.
Маршрут, що перехоплює все, володіє всім
Схема лише з DNS ніколи не додає addRoute("0.0.0.0", 0), тому до інтерфейсу TUN потрапляють тільки запити, а все інше далі використовує мережу під ним. Це безпечна форма, і вона ж пояснює, чому деякі списки дозволених працюють у Wi-Fi і відмовляють у стільниковій мережі.
У момент, коли існує маршрут за замовчуванням, кожен потік ваш. Пакет TCP, що надходить на інтерфейс і не пересилається та не отримує відповіді, стоїть у черзі, доки клієнту не вийде час, і помилки для журналу немає, бо нічого не зламалося. Я бачив, як людина витратила півдня на зламаний список дозволених, який насправді був відсутнім пересилачем для порту 443. Журналюйте протокол і призначення кожного пакета, який вирішите відкинути.
Сокет резолвера має вийти з тунелю
Пересилання — місце наступної пастки. Резолвери оператора часто живуть на приватній адресі, до якої тунель не дістає, тому пересланий запит іде нікуди, а застосунок повідомляє про вичерпання часу читання. Надсилайте схвалені запити резолверу, про який знаєте, що він доступний, і спершу передайте цей сокет у VpnService.protect(). Незахищений сокет повертає запит у ваш власний тунель, де він знову доходить до застосунку і крутиться, доки не спрацює тайм-аут.
Збій виглядає ідентично збою мережі, і тому так довго живе у звітах про помилки. Тримайте лічильники надісланих запитів і отриманих відповідей: якщо перше число зростає, а друге лишається нулем, ви дивитесь на сокет, а не на список.
Розмір і контрольні суми відмовляють без повідомлення
Тихі відкидання бувають і через розмір. Відповідь DNS із великим набором записів або рукостискання TCP зі щедрими опціями може перевищити MTU шляху, який ви ніколи явно не задавали; фрагмент зникає вище за потоком, і клієнт або повторює з меншим розміром, або перестає пробувати. Встановлення MTU інтерфейсу в 1280 — консервативний вибір, що витримує більшість стільникових шляхів, і явність краща за успадковане значення за замовчуванням, яке ніхто не міряв.
Якщо ви будуєте пакети самі, контрольна сума — другий тихий убивця. Контрольна сума UDP, обчислена за псевдозаголовком, у якому пропущено довжину UDP або поле протоколу, дає пакет, який у вас виглядає правильно сформованим, а отримувач відкидає його без сліду. Порівняйте свій псевдозаголовок зі специфікацією поле за полем і друкуйте обчислену контрольну суму для одного відомого потоку під час налагодження.
Чого VpnService за вас не зробить
Два обмеження варто знати, перш ніж проєктувати навколо них. Жоден звичайний застосунок не може встановити системний проксі або файл PAC поза Wi-Fi, і ця частина не змінилася. Шлях для окремих застосунків змінився: до API 29 VpnService.Builder взагалі не міг нести проксі, а від API 29 setHttpProxy(ProxyInfo.buildPacProxy(uri)) передає платформі скрипт PAC для обчислення для мережі VPN. Обчислюйте правила у власному коді, коли цілитеся на старіші API або коли модель PAC не може виразити правило. UDP теж не безкоштовний: браузери надсилають через проксі SOCKS лише TCP, а більшість клієнтів SOCKS5 ніколи не реалізують UDP ASSOCIATE, тому схема на основі проксі відкидає або пропускає UDP, поки TCP виглядає здоровим.
Саме це друге обмеження — причина, чому протокол на основі UDP може працювати у звичайній мережі й відмовити в момент підключення вашого VPN. Якщо тунель має нести UDP, пересилач мусить реалізувати шлях пересилання і для нього, а не лише шлях TCP з іншим типом сокета.
Перевіряйте в такому порядку
Пройдіть список по порядку і зупиніться на першому етапі, який відмовить, бо кожна наступна перевірка — шум, поки попередня не тримається.
- Перелічіть встановлені маршрути й підтвердьте, чи пакети, що не є DNS, узагалі потрапляють до інтерфейсу TUN.
- Запишіть у журнал ім'я запиту, рішення і код відповіді для одного заблокованого й одного схваленого імені.
- Підтвердьте, що сокет резолвера захищений і вибраний резолвер відповідає зсередини тунелю.
- Підтвердьте MTU інтерфейсу й контрольну суму однієї відповіді, зібраної вручну.
- Повторіть ту саму сторінку в Wi-Fi і в стільниковій мережі, бо резолвери оператора й MTU шляху різняться.
Якщо всі п'ять тримаються, а сторінка досі висить, наступний підозрюваний — сам пересилач: порівняйте перехоплений запит із перехопленою відповіддю і пошукайте відсутню кількість байтів або напівзакритий сокет. Довідка VpnService.Builder описує контракти маршруту, DNS і MTU, а protect() — окремий виклик VpnService, а не опція builder; підключений інтерфейс означає лише те, що перший контракт виконано.