# Важные нюансы работы встроенного lwIP TCP-стека Краткая памятка по тонким местам `src/lwip_tcp/`, критичным для прокси. Полное описание модуля — в `src/lwip_tcp/lwip_tcp_doc.md`. ## Ключевые константы (`lwip_tcp_opts.h`) | Константа | Значение | Смысл | |-----------|----------|-------| | `TCP_MSS` | 1460 | максимальный размер сегмента | | `TCP_WND` | 8×MSS = 11680 | окно приёма lwIP (объявляется пиру) | | `TCP_SND_BUF` | 16×MSS = 23360 | буфер отправки | | `TCP_TMR_INTERVAL` | 250 мс | базовый интервал таймера | | `tmr_interval_ms` | `TCP_TMR_INTERVAL/4` = 62.5 мс | фактический шаг `tcp_fasttmr` | | `TCP_WND_UPDATE_THRESHOLD` | `TCP_WND/4` = 2920 | порог немедленного ACK окна | **Важно:** таймер uasync использует timebase 0.1 мс — `uasync_set_timeout(ua, N, …)` задаёт N×0.1 мс. Например, 5000 = 500 мс. ## Контракт recv_cb — главный источник багов lwIP вызывает `recv_cb(arg, pcb, pbuf, err)` для данных, **уже извлечённых из pcb**. Дальше приложение само решает судьбу данных и окна: | Действие | Результат | |----------|-----------| | скопировать + `tcp_recved(pcb, len)` | данные приняты, окно восстановлено (норма) | | `return != LERR_OK` | lwIP кладёт pbuf в `refused_data` и отдаст позже — **без потери**, окно закрывается | | `pbuf_free(p)` без `tcp_recved` | данные потеряны, окно не восстановлено → схлопывание окна | `refused_data` — штатный backpressure lwIP: одноканальный (один pbuf), ре-доставка через `tcp_fasttmr` (62.5 мс) или при следующем входящем сегменте. **Ошибка, из-за которой падал `tcp_proxy_full`:** при backpressure прокси делал `pbuf_free(p)` без `tcp_recved`. Каждый такой сброс уменьшал `rcv_wnd` на MSS; после ~8 пакетов окно падало в 0, OS-TCP переставал слать, и передача умирала. ## Управление окном и «деградация 62.5 мс» `tcp_recved(pcb, len)` увеличивает `rcv_wnd` и через `tcp_update_rcv_ann_wnd` решает, слать ли ACK окна немедленно. Немедленный ACK идёт только если прирост окна `wnd_inflation ≥ TCP_WND_UPDATE_THRESHOLD` (=2920 = 2×MSS); иначе ACK откладывается до `tcp_fasttmr` (62.5 мс). Отсюда классическая ловушка: если после backpressure окно восстанавливать **по одному пакету** (`tcp_recved(1460)` за раз), прирост 1460 < 2920 → ACK уходит раз в 62.5 мс → пир шлёт 1 пакет за 62.5 мс ≈ 23 КБ/с. Это выглядит как «пропускная способность застряла», хотя сеть свободна. **Правильно:** восстанавливать окно **пачкой** (несколько `tcp_recved` подряд либо один `tcp_recved(TCP_WND_MAX - rcv_wnd)`), тогда прирост ≥ 2920 → ACK немедленный → пир сразу возобновляет бурст. ## Рекомендуемый паттерн для relay-потребителя (как в `tcp_proxy_client.c`) 1. При получении данных — копировать в ограниченную очередь, **не вызывать** `tcp_recved` (окно само плавно закрывается; очередь ограничена `TCP_WND`). 2. Дрейн очереди по сигналу освобождения нижележащего канала (`etcp_router_on_send_ready`) + retry-таймер (force=1) как safety-net. 3. `tcp_recved(len)` вызывать **только после успешной отправки** — окно восстанавливается пачкой по мере дрена, ACK уходит немедленно. 4. Никогда не сбрасывать pbuf без `tcp_recved`; не держать данные в `refused_data` дольше одного пакета (single-slot). См. реализацию: `tcp_proxy_client_tx_queue_drain_cb` / `tcp_proxy_client_fin_flush` в `src/proxy/tcp_proxy_client.c`.