Files
utun2/doc/lwip_nuances.md
T
evgeny f47ad7d87f tcp_proxy: fix throughput (tx_queue backpressure) + connect timeout + integration test
- Router: unified service delivery format [svc_id][src][dst][payload] (end-to-end src/dst from SVC_ROUTE header)
- tcp_proxy_client: replace single tx_buf drop-on-backpressure with bounded tx_queue + send_q waiter + retry timer (no data loss, bulk window reopen — removes 62.5ms delayed-ACK stall)
- tcp_io: configurable connect timeout (tcp_conn_set_connect_timeout)
- tcp_proxy_server: connect_timeout config option (ms, default 2000, 0=off)
- check.sh: run tcp_proxy_full integration test with sudo (skip if passwordless sudo unavailable)
- test_tcp_io: connect timeout unit test; docs (lwip_nuances.md)
2026-09-03 13:49:09 +03:00

4.7 KiB
Raw Blame History

Важные нюансы работы встроенного 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.