From b4bcc051622c88c7b81361008b35dd44cefd6303 Mon Sep 17 00:00:00 2001 From: iRedTea Date: Fri, 16 Oct 2026 22:02:59 +0300 Subject: [PATCH] docs: a plan for flow control in the direct tunnel, for when one slow connection stalling an agent's others starts to matter --- docs/plans/tunnel-flow-control.md | 73 +++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 docs/plans/tunnel-flow-control.md diff --git a/docs/plans/tunnel-flow-control.md b/docs/plans/tunnel-flow-control.md new file mode 100644 index 0000000..8117f4b --- /dev/null +++ b/docs/plans/tunnel-flow-control.md @@ -0,0 +1,73 @@ +# План: управление потоком в прямом туннеле (tcp-проброс) + +Статус: **отложено** - сделать, если на практике упрёмся. Записано 2026-10-16. + +## Что сейчас + +Прямой туннель (`:8446`, режим проброса `tcp`) - одно TCP-соединение на агента, по нему +мультиплексируются все проброшенные соединения этого агента кадрами +`type(1) | stream(4) | length(4) | payload` (`internal/tunnel/frame.go`). + +Ёмкости хватает с запасом: на агента - одно соединение и одна горутина чтения, на проброшенное +соединение - горутина и буфер 32 КБ; шифрования в туннеле нет. 20 агентов и тысячи соединений - +десятки МБ памяти и почти ноль CPU. Потолок - сеть хоста (1 Гбит/с), через которую весь трафик +идёт дважды (пользователь ↔ хост ↔ агент). + +## Проблема: блокировка всего туннеля агента одним соединением + +Управления потоком (окон, как в HTTP/2 или yamux) нет, поэтому внутри одного агента медленное +соединение тормозит остальные: + +- **Хост** (`internal/server/tunnelserver.go`, `agentTunnel.readLoop`): кадр `TypeData` пишется + пользователю синхронно, `c.Write(f.Payload)`, без дедлайна. Клиент, который медленно читает или + завис, останавливает чтение туннеля - встают все пробросы этого агента, пока клиент не отвалится. +- **Агент** (`internal/agent/tunnel.go`, `tunClient.readLoop`): данные кладутся в канал потока + `in` (256 кусков, до ~8 МБ). Если локальный сервис не читает и канал полон, `readLoop` + блокируется на `s.in <- f.Payload` - встаёт весь туннель агента. + +Между агентами влияния нет: у каждого свой `readLoop`. + +Когда проявится: через одного агента одновременно большая отдача медленному клиенту и что-то +интерактивное (SSH, веб) - интерактивное подвисает. Худший случай - зависший клиент держит +туннель агента до разрыва TCP. + +## Предлагаемое решение + +1. **Своя очередь записи у каждого потока** - на хосте и на агенте. `readLoop` только кладёт + кадр в очередь потока и никогда не пишет в сокет сам; пишет отдельная горутина потока. +2. **Окно (кредиты) на поток.** Новый тип кадра `TypeWindow` (stream, uint32 байт): получатель + сообщает, сколько ещё байт готов принять. Отправитель не шлёт `TypeData` сверх окна. + Начальное окно - 256 КБ; получатель возвращает кредит по мере записи в сокет (например, когда + записана половина окна). Так очередь потока ограничена окном, а медленный поток тормозит + только себя. +3. **Дедлайны записи.** Запись в пользовательский или локальный сокет - с `SetWriteDeadline` + (например, 60 с без прогресса). Не успел - поток закрывается (`TypeClose`), туннель живёт. +4. **Справедливость отправки.** Отправка в туннель уже сериализована (`wmu`); кадры данных + ограничить 32 КБ (сейчас так и есть), чтобы большой поток не занимал канал надолго. + +## Совместимость со старыми агентами + +Рукопожатие `LPRA-TUN1` не меняем: старый агент отвергает другое приветствие, а старый сервер - +ответ не из двух полей. Вместо этого: + +- сервер знает версию агента (из БД, `versionAtLeast`) и, если агент новый, сразу после + рукопожатия шлёт кадр `TypeHello` (флаги: `flowControl`); +- агент включает окна, только получив `TypeHello`. Старый сервер его не шлёт - новый агент + работает с ним по-старому. Старому агенту сервер `TypeHello` не шлёт и окна не включает (а + пришедший неизвестный кадр старый агент и так пропустил бы: в его `readLoop` нет `default`). + +Новые типы кадров: `TypeHello = 5`, `TypeWindow = 6`. + +## Тесты + +- Медленный читатель на одном потоке не задерживает другой поток того же агента (замер задержки + эха на втором потоке при забитом первом). +- Зависший клиент закрывается по дедлайну, туннель и остальные потоки живы. +- Новый сервер + старый агент и старый сервер + новый агент - проброс работает, как раньше. +- Пропускная способность одного потока не падает заметно (окно 256 КБ на RTT до 50 мс даёт + ~40 Мбит/с на поток; если мало - окно больше или автоподстройка). + +## Объём + +Сервер: `tunnelserver.go` (~150 строк), агент: `tunnel.go` (~150 строк), `frame.go` (типы), тесты. +Нужна новая версия агента; панель после обновления помечает старых как устаревших.