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