Android VpnService: зависшее чтение TUN против ошибок записи ENOBUFS
Чтение, которое никогда не возвращается, и запись, которая завершается с ENOBUFS, имеют противоположные причины. Измерьте объявленное окно и длину пакета, чтобы выбрать исправление.
В одном и том же цикле VpnService появляются два симптома, и оба валят на одно и то же: читатель, который никогда не возвращается, и запись, которая завершается с ENOBUFS. Это не разновидности одной ошибки. Зависшее чтение — это разногласие об управлении потоком с партнёром; неудачная запись — это пакет, который интерфейс не примет. Если относиться к ним как к одной проблеме, вы потратите день на настройку буферов, которые никогда не участвовали. Разделение ниже — это то, как я решаю, какую половину цикла инструментировать первой.
Один цикл, две независимые точки отказа
Ваш сервис читает IP-пакет из дескриптора файла TUN, проверяет его и пишет ответ обратно в тот же дескриптор. Всё, что ломается на этом пути, проявляется в одном из этих двух вызовов, поэтому оба и объявляются сломанным туннелем. У вызовов разные контракты. Чтение ждёт, пока ядро выдаст вам пакет, а запись отдаёт ядру пакет, который оно имеет право отклонить.
Чтения — это лёгкая половина для размышлений. Чтение, которое ничего не возвращает, либо ждёт трафик, который никогда не придёт, либо ждёт, потому что партнёр перестал отправлять.
Зависшее чтение — это разногласие об управлении потоком
Чтение устройства TUN блокируется, пока пакет не станет доступен. Это задуманное поведение, и поэтому поток, стоящий в read, выглядит нормально на простаивающем канале. Это становится зависанием, когда ваш собственный стек TCP продолжает принимать данные от локального приложения, но так и не объявляет окно, достаточное для того, чтобы удалённый партнёр отправил больше. Партнёр заполняет объявленное вами окно, ждёт ACK и обновления окна и останавливается. Тогда вашему читателю нечего вернуть, и приложение видит открытое и молчащее соединение.
Обычные причины — стек, который пишет в сокет, не отслеживая объявленное окно, и другой, который забывает отправить обновление окна после опустошения буфера. Снаружи они выглядят одинаково: байты уходят, ничего не возвращается, ошибка не возникает. Блокировка потока чтения на разборе, разрешении DNS или дисковом вводе-выводе даёт ту же форму симптома по несвязанной причине, поэтому убедитесь, что поток действительно внутри read, прежде чем винить управление потоком.
Проверьте очередь отправки партнёра и окно, которое он видел последним. Если партнёр держит данные из-за того, что ваше окно равно нулю, исправление в вашем пути ACK, а не в чтении TUN.
ENOBUFS — это проблема размера, а не нехватка буфера
Запись, которая возвращает ENOBUFS на интерфейсе TUN, обычно отвергает длину пакета. У интерфейса есть MTU, более крупный пакет нельзя поставить в очередь, и повторная попытка того же пакета каждый раз терпит неудачу одинаково. Эта детерминированность и есть подсказка. Настоящая нехватка буфера исчезает при снижении давления, а отказ по размеру не зависит от того, сколько вы ждёте.
У TCP есть механизм именно для этого, и это шаг, который люди пропускают: ограничение MSS. Когда вы синтезируете сегменты в пользовательском пространстве, вы выбираете размер полезной нагрузки, и выбор его из MTU локального интерфейса вместо MTU туннеля выдаёт сегменты, которые туннель не может пронести. Ограничьте меньшим из двух. Ограничение покрывает только TCP, поэтому слишком большую UDP-датаграмму ваш код должен фрагментировать или отбросить.
Записывайте длину каждого пакета, который пишете, во время отладки. Длина, на которой происходит сбой, и есть ответ, и она не изменится между попытками.
Три измерения, которые разделяют случаи
Вам не нужен отладчик ни для одного из двух отказов. Трёх наблюдений достаточно, чтобы направить себя в нужный раздел, и они занимают меньше времени, чем пересборка.
| What you see | Where to look |
|---|---|
| Reader parked, peer send queue empty, last advertised window zero | Window updates in your ACK path |
| Write fails on one length and succeeds on a smaller one | MSS clamp and tunnel MTU |
| Both directions look fine, app still sees no data | Protected 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, работающий только до первого изменения сети, — это проблема маршрутизации, а не управления потоком.