# План: управление потоком в прямом туннеле (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` (типы), тесты. Нужна новая версия агента; панель после обновления помечает старых как устаревших.