В одном и том же цикле 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, работающий только до первого изменения сети, — это проблема маршрутизации, а не управления потоком.

Похожие статьи