У тому самому циклі VpnService виникають два симптоми, і обидва звалюють на одне й те саме: читач, який ніколи не повертається, і запис, що завершується з ENOBUFS. Це не різновиди однієї помилки. Зависле читання — це розбіжність щодо керування потоком із партнером; невдалий запис — це пакет, який інтерфейс не прийме. Якщо ставитися до них як до однієї проблеми, ви витратите день на налаштування буферів, які ніколи не брали участі. Поділ нижче — це те, як я вирішую, яку половину циклу інструментувати першою.

Один цикл, дві незалежні точки відмови

Ваш сервіс читає IP-пакет із дескриптора файлу TUN, перевіряє його і пише відповідь назад у той самий дескриптор. Усе, що ламається на цьому шляху, проявляється в одному з цих двох викликів, тому обидва й оголошуються зламаним тунелем. Виклики мають різні контракти. Читання чекає, поки ядро видасть вам пакет, а запис віддає ядру пакет, який воно має право відхилити.

Читання — це легша половина для міркувань. Читання, яке нічого не повертає, або чекає трафік, який ніколи не прийде, або чекає, бо партнер перестав надсилати.

Зависле читання — це розбіжність щодо керування потоком

Читання пристрою TUN блокується, доки пакет не стане доступним. Це задумана поведінка, і тому потік, що стоїть у read, виглядає нормально на простоюючому каналі. Це стає зависанням, коли ваш власний стек TCP далі приймає дані від локального застосунку, але так і не оголошує вікно, достатнє для того, щоб віддалений партнер надіслав більше. Партнер заповнює оголошене вами вікно, чекає ACK і оновлення вікна та зупиняється. Тоді вашому читачеві нічого повертати, і застосунок бачить відкрите й мовчазне з'єднання.

Звичайні причини — стек, який пише в сокет, не відстежуючи оголошене вікно, і інший, який забуває надіслати оновлення вікна після спорожнення буфера. Ззовні вони виглядять однаково: байти йдуть, нічого не повертається, помилка не виникає. Блокування потоку читання на розборі, розв'язанні DNS або дисковому вводі-виводі дає ту саму форму симптому з не пов'язаної причини, тому переконайтеся, що потік справді всередині read, перш ніж звинувачувати керування потоком.

Перевірте чергу надсилання партнера та вікно, яке він бачив останнім. Якщо партнер тримає дані через те, що ваше вікно нульове, виправлення в вашому шляху ACK, а не в читанні TUN.

ENOBUFS — це проблема розміру, а не брак буфера

Запис, який повертає ENOBUFS на інтерфейсі TUN, зазвичай відхиляє довжину пакета. Інтерфейс має MTU, більший пакет не можна поставити в чергу, і повторна спроба того самого пакета щоразу зазнає невдачі однаково. Ця детермінованість і є підказкою. Справжній брак буфера зникає, коли тиск спадає, а відмова за розміром не зважає на те, скільки ви чекаєте.

TCP має механізм саме для цього, і це крок, який люди пропускають: обмеження MSS. Коли ви синтезуєте сегменти в просторі користувача, ви обираєте розмір корисного навантаження, і вибір його з MTU локального інтерфейсу замість MTU тунелю видає сегменти, які тунель не може пронести. Обмежте меншим із двох. Обмеження покриває лише TCP, тому завелику UDP-датаграму ваш код має фрагментувати або відкинути.

Записуйте довжину кожного пакета, який пишете, під час налагодження. Довжина, на якій стався збій, і є відповіддю, і вона не зміниться між спробами.

Три вимірювання, що розділяють випадки

Вам не потрібен налагоджувач для жодної з двох відмов. Трьох спостережень достатньо, щоб спрямувати себе до потрібного розділу, і вони займають менше часу, ніж перезбирання.

What you seeWhere to look
Reader parked, peer send queue empty, last advertised window zeroWindow updates in your ACK path
Write fails on one length and succeeds on a smaller oneMSS clamp and tunnel MTU
Both directions look fine, app still sees no dataProtected socket and routing loop

Якщо підходить третій рядок, сам цикл у порядку, а проблема там, де тунель її не бачить: незахищений транспортний сокет, який несе ваш власний VPN-трафік назад у тунель, що зазвичай виглядає як з'єднання, яке встановлюється, а потім нічого не доставляє.

Полгодьте сантехніку, від якої залежать обидва шляхи

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

Builder()
  .addAddress(10.8.0.2, 32)
  .addRoute(0.0.0.0, 0)
  .setMtu(1400)
  .establish()

Встановіть MTU на реальне значення тунелю, а не залишайте те, що використовував приклад. Усе, що ви напишете пізніше, обмежене цим числом, і розбіжність між заданим MTU і MSS, до якого ви обмежуєте, — найпоширеніший спосіб повернути помилку запису після виправлення. Потім захистіть сокет, який несе трафік до самого VPN-сервера. Без protect ці пакети маршрутизуються назад у ваш власний тунель, і з'єднання ніколи не завершується, що виглядає як зависле читання, але є петлею маршрутизації.

Згоду не можна обійти. Сервіс, запущений без того, щоб prepare повернув null, не отримує інтерфейс, а establish повертає null замість винятку, тому перевірте дескриптор перед запуском циклу. Довідник VpnService — це місце, де підтвердити контракт builder замість копіювання прикладу.

Перевіряйте один напрямок за раз

Надішліть UDP-луну через тунель і спостерігайте за довжинами пакетів, перш ніж торкатися TCP. Потім відкрийте рукостискання і переконайтеся, що сегменти залишаються нижче обчисленого вами MSS. Рахуйте пакети, а не з'єднання, щоб зависання проявилося як розрив у лічильнику, а не як висячий сокет. Якщо читання зупиняються після сплеску записів, підозрюване вікно; якщо записи зриваються одразу, то довжина.

Потім перезавантажте пристрій і повторіть один раз, бо VpnService, який працює лише до першої зміни мережі, — це проблема маршрутизації, а не керування потоком.

Схожі статті