Чому Android VpnService перехоплює частину трафіку, а не весь
Маршрути, записи DNS і область дії за застосунками визначають, які пакети доходять до пристрою tun, чому локальні пристрої зникають і від чого відмовляється конструктор.
Ніщо у VpnService не перехоплює трафік саме по собі. Він просить у системи інтерфейс tun, передає цьому інтерфейсу набір адрес і маршрутів, і з цього моменту ядро копіює кожен пакет, що відповідає цим маршрутам, у файловий дескриптор, який застосунок може читати. Код застосунку ніколи не бачить пакети, які не обрали маршрути, тому охоплення тунелю — це питання таблиці маршрутизації раніше, ніж питання програмування. Тому та сама реалізація служби може проковтнути весь потік на одному телефоні й лише частину на іншому, і тому додавання маршруту іноді лагодить тунель, який виглядав зламаним. Ви отримуєте те, що оголосили, мінус усе, від чого конструктор тихо відмовився.
Маршрути вирішують, які пакети надходять
Builder.addRoute(prefix, prefixLength) встановлює префікс у таблицю маршрутизації мережі VPN. Система вважає цю мережу типовою для застосунків в області дії, тому її записи переглядаються раніше за типовий маршрут фізичної мережі, і відповідного префікса достатньо, щоб відхилити потік. Тому маршрут для 0.0.0.0/0 забирає IPv4 повністю, включно з принтером, ціллю cast і сторінкою адміністрування маршрутизатора.
Клієнти, яким потрібне таке покриття без видалення фізичного типового запису, використовують натомість пару 0.0.0.0/1 і 128.0.0.0/1. Два конкретніші маршрути перемагають типовий маршрут, разом вони покривають увесь адресний простір, а власний типовий запис телефону залишається в таблиці для сокетів, досі прив'язаних до фізичної мережі.
Адреси й записи DNS надходять від того самого конструктора й відповідають на інше питання. addAddress дає інтерфейсу адресу з довжиною префікса, а addDnsServer встановлює розв'язувач, який застосунки в області дії запитуватимуть через інтерфейс. Обидві родини можуть бути на одному інтерфейсі, тому набір маршрутів лише для IPv4 поруч з адресою IPv6 — це конфігурація, яка читається правильно й усе одно протікає. Якщо ви маршрутизуєте родину, у якій інтерфейс не має адреси, ядро не має адреси джерела для цих пакетів, і вони ніколи не з'являться біля дескриптора.
Що повертає establish()
establish() повертає ParcelFileDescriptor або null, коли застосунок не авторизовано. Null — це стан згоди, а не помилка мережі: VpnService.prepare() уже мав повернути null, або користувач мав підтвердити системний діалог до виклику. Згоду можна відкликати під час роботи служби, і інтерфейс закривається під вами без жодного зворотного виклику у ваш цикл читання.
Дескриптор не блокує, якщо не вказано інше, тому читання повертається одразу без даних, а не паркує потік. Цикл, написаний для блокуючого сокета, спалює одне ядро, а цикл, що вважає порожнє читання фатальним, розриває тунель за порожню секунду. Builder.setBlocking(true) змінює цю поведінку, і ціна — потік, який чекає всередині читання.
Аргументи маршруту й адреси перевіряються як значення LinkAddress, а multicast-призначення на кшталт 224.0.0.0/4 відхиляються одразу. Інтерфейс tun точка-точка також не має домену широкомовлення, тому виявлення multicast через нього потребує іншого проєкту, а не ще одного маршруту. Довідник VpnService.Builder — це список, який варто перевірити, перш ніж робити висновок, що виклик проігноровано.
Область за застосунками звужує набір, а не тунель
Від API 21 конструктор приймає addAllowedApplication і addDisallowedApplication, і ці два списки взаємовиключні в межах одного конструктора. Список дозволених робить тунель наявним для цих пакетів, поки все інше зберігає фізичну мережу; список заборонених перехоплює все, крім названих пакетів, що клієнт пропонує як розділене тунелювання. До API 21 платформного механізму немає взагалі, а поведінка за застосунками надходила від сторонніх клієнтів, які фільтрували за UID у власному пересилачі. Різниця важлива під час налагодження, бо тунель за застосунками зі списком дозволених з одного пакета майже не дає трафіку, і це правильна поведінка, а не зламаний маршрут.
Яким би не було охоплення, сокет, який ваш застосунок відкриває до сервера VPN, не повинен проходити через тунель, який він будує. VpnService.protect(socket) прив'язує цей сокет до нижчої мережі, щоб його пакети оминали пристрій tun. Пропустіть виклик, і рукостискання проковтне той інтерфейс, який мав його підняти.
VpnService.setUnderlyingNetworks замикає цикл, повідомляючи системі, яка фізична мережа несе тунель. Це значення визначає, як VPN повідомляє про з'єднання іншим застосункам, і null або застаріле значення — звичайна причина під'єднаного тунелю, який решта телефону вважає офлайн. Оновлюйте його, коли змінюється нижча мережа, а не лише під час запуску.
Контрольні суми й MTU: де правильний маршрут усе одно не працює
Коли пакети надходять, дві деталі на рівні пакета визначають, чи щось працює від краю до краю. Перша — адреса джерела: за кількох адрес інтерфейсу ядро може надсилати пакети, що несуть першу, тому пересилач, який переписує заголовки, має виправити адресу й контрольну суму, що її покриває.
Друга — охоплення контрольної суми. Коли застосунок сам будує пакети TCP або UDP, контрольна сума покриває псевдозаголовок з адресою джерела, адресою призначення, номером протоколу й довжиною транспорту, і пропуск будь-чого з цього дає пакети, що надходять і відкидаються. Ця невдача виглядає як тайм-аут після відповіді, яка ніколи не надходить, і це повертає більшість людей до маршрутів. setMtu фіксує MTU інтерфейсу, і кожне читання повертає рівно один повний пакет IP, тому буфер, написаний для потоку байтів, фрагментує ваш розбір, а не пакет.
Перевірний прохід, який розділяє рівні
Щоб знайти, де охоплення пішло не так, перевіряйте рівні по черзі. Запитайте пристрій, які маршрути мережа VPN справді тримає, потім перевірте, чи потік до відомої адреси з'являється як читання біля дескриптора, потім перевірте, чи правильно сформований пакет, який надсилає ваш пересилач. Проблема маршруту нічого не показує біля дескриптора, тоді як проблема контрольної суми показує пакет, що надходить і зникає.
Якщо до дескриптора нічого не доходить, підозрюваний — набір маршрутів, а не читач: підтвердьте префікси, підтвердьте наявність адреси в тій самій родині й підтвердьте, що застосунок у межах заданої вами області. Якщо пакети доходять до дескриптора, але нічого не повертається, перегляньте protect() і значення нижчої мережі, перш ніж знову торкатися маршруту. Якщо далека сторона повідомляє про пошкоджені пакети, контрольна сума й MTU — це місця, куди дивитися. Одна перевірка на рівень економить півдня здогадів.