Два шляхи можуть поставити IP-інтерфейс перед Android-застосунком, і обидва закінчуються на тому самому типі файлового дескриптора з дуже різними власниками. Один іде через VpnService і його builder, другий через root-оболонку і /dev/net/tun. Кожне читання з цього дескриптора — вихідний пакет, кожен запис — вхідний. Шляхи розділяє те, хто тримає політику, що переживає оновлення ОС і наскільки можна торкатися таблиці маршрутизації.

Обирай шлях платформи, якщо тільки не можеш назвати конкретну річ, від якої вона відмовляється. Ця відмова зазвичай TAP, перехоплення поза власним набором застосунків або правило маршрутизації, якого фреймворк ніколи не розкриває.

На шляху платформи ти оголошуєш намір замість команд. Ти вказуєш принаймні одну адресу, потрібні маршрути, DNS-сервери і необов'язковий список застосунків, потім системна служба Android виконує привілейоване налаштування під власною ідентичністю. Ти ніколи не дізнаєшся ім'я інтерфейсу заздалегідь і ніколи не видаєш 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 — інша вісь

Запит builder перетинає межу системи через Binder, і десь нижче платформа викликає нативний код, щоб налаштувати пристрій. Це два різні механізми, складені в один виклик. Binder — міжпроцесна взаємодія між застосунком і системною службою, а JNI — міст із Java у нативний код усередині процесу. Читання дерева AOSP Android 9 робить стек буквальним, і варто пам'ятати, що внутрішні імена класів і шляхи до файлів змінюються між випусками.

Що не змінюється — це контракт, на який можна спертися: builder, дескриптор, який він повертає, і життєвий цикл служби навколо нього. Жоден застосунок не повинен викликати внутрішню транзакцію служби або перестворювати вузол пристрою, щоб отримати інтерфейс. Якщо твій код залежить від імені класу з AOSP, він зламається на випуску, який ти не тестував.

TUN несе IP, TAP несе Ethernet

TUN — це IP-інтерфейс. TAP — це Ethernet-інтерфейс, і ця різниця вирішує, чи можливе мостове об'єднання взагалі. У TUN-пристрою немає MAC-адреси і немає заголовка Ethernet для пересилання, тому TUN-клієнт не може увійти в міст рівня 2, як би не були написані маршрути.

VpnService дає тобі варіант IP, який потрібен VPN рівня застосунку і який він може підтримувати без сирого стеку Ethernet. Якщо твій дизайн вимагає моста, шлях платформи туди не приведе, а шлях root — лише якщо ядро й дозволи пристрою співпрацюватимуть.

Де шлях root усе ще виграє

Три завдання відводять людей зі шляху платформи. Мосту рівня 2 потрібен TAP. Перехоплення трафіку застосунків поза твоєю зоною VPN вимагає видимості, якої фреймворк не дає. Маршрутизація за політикою, яку builder ніколи не розкриває, наприклад окрема таблиця для одного призначення, вимагає правил, які ти встановлюєш сам.

Витрати приходять пізніше: розблокований bootloader або образ із root, політика SELinux, що з'їжджає під тобою, і пристрій, який після OTA перестає поводитися правильно. Рецепт із root ще й не можна розповсюджувати через звичайну дистрибуцію застосунків, тому більшість команд тримають його як внутрішній діагностичний шлях, а не як продукт.

Вибір між ними

Починай з інтерфейсу, який потрібен, а не з коду, який є. Якщо відповідь — пристрій TUN із маршрутами, обмеженими застосунком, шлях платформи коротший і переживає оновлення. Якщо відповідь включає кадри Ethernet, перехоплення інших застосунків або правила маршрутизації, які builder відхиляє, ти на території root і маєш закласти це в бюджет як операційні витрати.

Перевірка, яка вирішує більшість суперечок: спершу побудуй шлях платформи, запиши результат establish() окремо від стану віддаленої сесії й підтвердь по одному пакету в кожному напрямку. Лише потім вирішуй, чи виправдовує відсутня можливість вихід із підтримуваного шляху.

Схожі статті