Browse Source
- 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)v2
36 changed files with 755 additions and 266 deletions
@ -0,0 +1,115 @@ |
|||||||
|
# Задача: продолжить исправление TCP-proxy (крупные файлы через транзит) |
||||||
|
|
||||||
|
Продолжение незавершённой работы. Читай целиком, прежде чем что-то менять. |
||||||
|
|
||||||
|
## 1. Что уже сделано (НЕ переделывать) |
||||||
|
|
||||||
|
Унифицирован формат доставки сервисных кодограмм роутером. Раньше сервисы определяли |
||||||
|
источник через `conn->peer_node_id`, что ломало транзит (при промежуточном узле это был |
||||||
|
id промежуточного, а не реального клиента). |
||||||
|
|
||||||
|
Теперь роутер доставляет сервисам единый формат: |
||||||
|
|
||||||
|
``` |
||||||
|
[svc_id(1)][src_node_id(8)][dst_node_id(8)][payload...] |
||||||
|
``` |
||||||
|
|
||||||
|
где src/dst — реальные end-to-end узлы из SVC_ROUTE заголовка. |
||||||
|
|
||||||
|
Ключевые константы в `src/routing_layer/etcp_router.h`: |
||||||
|
`ROUTER_SVC_SRC_OFF=1`, `ROUTER_SVC_DST_OFF=9`, `ROUTER_SVC_PAYLOAD_OFF=17`, `ROUTER_SVC_HDR_SIZE=17`. |
||||||
|
|
||||||
|
Изменённые файлы (все в некоммиченном состоянии, `git status`): |
||||||
|
- `src/routing_layer/etcp_router.{h,c}` — `router_deliver` + `router_deliver_loopback` + CLOSE/RESTART |
||||||
|
- `src/proxy/tcp_proxy_server.{h,c}` — recv +8→+16 сдвиг, `src_node_id` из `dgram[1..8]` |
||||||
|
- `src/proxy/tcp_proxy_client.{h,c}` — recv сдвиг + счётчики `bytes_to_exit/from_exit/bp_count` |
||||||
|
- `src/proxy/udp_proxy.{h,c}`, `src/proxy/icmp_proxy.{h,c}` — сдвиг + убран встроенный node_id |
||||||
|
- `src/nat_transport.c` — убран встроенный node_id |
||||||
|
- `src/routing_layer/routing.c`, `conn_mgr_core.c` — сдвиг |
||||||
|
- `src/media_delivery/media_delivery.c` — сдвиг + `from_node` из `dgram[1..8]` |
||||||
|
- `tools/chatgui/transport/utun_node.cpp` — сдвиг |
||||||
|
- тесты `tests/test_etcp_router*.c`, `test_udp_proxy.c`, `test_icmp_proxy.c`, `test_nat_transport.c` |
||||||
|
- `tests/tcp_proxy_full/` — добавлен `intermediate.conf` (3-узловая топология), обновлены `client.conf`/`exit.conf`/`run_test.sh` |
||||||
|
|
||||||
|
**Статус проверки: `./check.sh` = 73 passed, 0 failed, 1 skipped.** Форматная часть ВЕРНА. |
||||||
|
|
||||||
|
## 2. Что осталось (собственно задача) |
||||||
|
|
||||||
|
Интеграционный тест прокси `tests/tcp_proxy_full/run_test.sh` (нужен sudo) — крупные |
||||||
|
передачи НЕ проходят. Проверено: проблема **НЕ в формате и НЕ в транзите** — прямой |
||||||
|
2-узловой прогон падает идентично. Это предсуществующая проблема прокси/lwIP. |
||||||
|
|
||||||
|
Результат `sudo ./tests/tcp_proxy_full/run_test.sh`: |
||||||
|
- `idle` (32 КБ) — PASS |
||||||
|
- `half_close` (64 КБ) — 64280/65536 (почти, теряется хвост) |
||||||
|
- `basic_1mb` (1 МБ), `concurrent_*`, `stress` — FAIL (timeout) |
||||||
|
|
||||||
|
## 3. Диагноз (уже установлен по логам) |
||||||
|
|
||||||
|
Пропускная способность ~24 КБ/с (1 пакет 1460 байт за ~60 мс). Для 1 МБ это ~43 с → таймаут 10 с. |
||||||
|
|
||||||
|
Цепочка (видна в добавленных логах `PROXY BP *` и `SEND_Q_ACKED`): |
||||||
|
1. Клиент быстро шлёт 320 пакетов (initial burst ~33 МБ/с). |
||||||
|
2. Роутерный `send_q` переполняется (`router_send_q: FULL count=64`) → `etcp_route_send` возвращает -1. |
||||||
|
3. Прокси в `tcp_proxy_client_recv_cb` ставит одиночный `tx_buf` и **сбрасывает** дальнейшие данные |
||||||
|
(`if (pc->tx_buf) { pbuf_free(p); return; }`) — лог `PROXY BP drop`, без `tcp_recved` → окно lwIP схлопывается. |
||||||
|
4. Клиент вырождается в режим «1 пакет за ~60 мс». |
||||||
|
5. ~60 мс = таймер lwIP: `ctx->tmr_interval_ms = TCP_TMR_INTERVAL / 4` = 62.5 мс (`src/lwip_tcp/lwip_tcp.c:106`). |
||||||
|
В режиме «пакет-за-пакетом» lwIP не шлёт ACK сразу (порог `TCP_WND_UPDATE_THRESHOLD = TCP_WND/4 = 2920` |
||||||
|
не набирается за 1 пакет), и отложенный ACK уходит только по `tcp_fasttmr` раз в 62.5 мс. |
||||||
|
|
||||||
|
Триггер — сброс данных при backpressure в прокси (`tcp_proxy_client.c`), НЕ lwIP. |
||||||
|
|
||||||
|
## 4. Ограничения (ВАЖНО) |
||||||
|
|
||||||
|
- **В `src/lwip_tcp/` ничего НЕ править без явного согласования пользователя. Разрешён только debug-лог.** |
||||||
|
- Правки исходников только через Edit tool (никакого sed для массовых замен). |
||||||
|
- Ошибки/нештатные ветки — обязательно `DEBUG_ERROR/DEBUG_WARN`. |
||||||
|
- Логи информативные, без спама. Debug-категории: `proxy`, `etcp_route`. |
||||||
|
- Перед сборкой `make clean`. |
||||||
|
|
||||||
|
## 5. План продолжения |
||||||
|
|
||||||
|
Шаг 1 (диагностика, закрыть пробел — по желанию, уже почти доказано): |
||||||
|
- Добавить в lwIP только DEBUG-лог (разрешено): в `tcp_fasttmr` — счётчик отложенных ACK; в `tcp_recved` — `rcv_wnd`. |
||||||
|
- В прокси (`tcp_proxy_client.c`, НЕ lwIP) в момент `BP drop` лог `pcb->rcv_wnd`. |
||||||
|
- Подтвердить, что 62.5 мс — это отложенный ACK. |
||||||
|
|
||||||
|
Шаг 2 (фикс, требуется согласование — предложи пользователю): |
||||||
|
- **Вариант A (предпочтительно, НЕ lwIP):** в прокси заменить одиночный `tx_buf` + сброс на очередь с backpressure |
||||||
|
(буферизовать все пакеты во время backpressure, не сбрасывать; `tcp_recved` вызывать по мере отправки). |
||||||
|
Тогда окно закрывается плавно, OS TCP не сваливается в congestion avoidance, деградации нет. |
||||||
|
Смотри `struct tcp_proxy_client_conn` в `tcp_proxy_client.h` (сейчас: `tx_buf`/`tx_len`/`tx_waiter`). |
||||||
|
Для образца очереди с backpressure смотри `src/proxy/tcp_proxy_server.c` (там `read_queue` + `pause_waiter`). |
||||||
|
- **Вариант B (lwIP, только по согласованию):** уменьшить `tmr_interval_ms` (`TCP_TMR_INTERVAL/4` → чаще), |
||||||
|
либо повысить `TCP_WND_UPDATE_THRESHOLD`/`TCP_WND`, чтобы ACK слался сразу. |
||||||
|
|
||||||
|
Шаг 3 (почистить тестовую обвязку): |
||||||
|
- В `tests/tcp_proxy_full/*.conf` сейчас включены отладочные уровни (`etcp_route=trace`, `proxy=trace`) — |
||||||
|
перед коммитом вернуть к разумным (`etcp_route=debug`/`proxy=debug` или `error`). |
||||||
|
- Решить: оставить добавленные в `tcp_proxy_client.c` счётчики/BP-логи или вычистить. |
||||||
|
- В `run_test.sh` тайминги `elapsed` через `date +%s%3N` дают мусор в этой среде — поправить (например `date +%s` |
||||||
|
без миллисекунд, или `SECONDS`). Stress-таймаут `60s` можно уменьшить до ~15-20s. |
||||||
|
|
||||||
|
## 6. Сборка и тест |
||||||
|
|
||||||
|
```bash |
||||||
|
./build.sh --full -j4 # или make -j4 (после make clean) |
||||||
|
./check.sh # unit-тесты: должны остаться 73 passed |
||||||
|
cp src/utun utun # обновить корневой бинарник для run_test.sh! |
||||||
|
sudo ./tests/tcp_proxy_full/run_test.sh # интеграционный (нужен root/TUN/iptables) |
||||||
|
``` |
||||||
|
|
||||||
|
Важно: `run_test.sh` использует `UTUN_BIN="$SCRIPT_DIR/../../utun"` (корневой `utun`), а `make` собирает |
||||||
|
`src/utun`. После каждой пересборки делать `cp src/utun utun`. |
||||||
|
|
||||||
|
## 7. Как быстро убедиться, что фикс помог |
||||||
|
|
||||||
|
Прогнать `basic_1mb` и смотреть лог клиента `tests/tcp_proxy_full/log/client_utun.log`: |
||||||
|
- ДО фикса: `PROXY BP drop` + `SEND_Q_ACKED` с интервалом ~60 мс. |
||||||
|
- ПОСЛЕ фикса: нет `BP drop`, интервал ACK ~10 мс, `basic_1mb` PASS. |
||||||
|
|
||||||
|
Полезные grep-и по логу клиента: |
||||||
|
``` |
||||||
|
grep -E "PROXY BP|SEND_Q_ACKED|PROXY FIN" client_utun.log |
||||||
|
``` |
||||||
@ -0,0 +1,66 @@ |
|||||||
|
# Важные нюансы работы встроенного 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`. |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
[global] |
||||||
|
my_node_id=0x628471cde1291456 |
||||||
|
my_private_key=2012fcb6f33004ee64c899a229c54619ed89f83d8fe7e003c4d2eca43c453b58 |
||||||
|
my_public_key=23ea42d345a0efbcfaab4f47c27bf6da05e3682e27a667ba60a16645ce2c9024 |
||||||
|
tun_ip=10.200.40.1/24 |
||||||
|
tun_ifname=tun_test_inter |
||||||
|
debug_level=error |
||||||
|
|
||||||
|
[server: s1] |
||||||
|
addr=127.0.0.1:15003 |
||||||
|
type=public |
||||||
|
|
||||||
|
[allowed_keys] |
||||||
|
allow_all=1 |
||||||
|
|
||||||
|
[client: c1] |
||||||
|
keepalive=1 |
||||||
|
link=s1:127.0.0.1:15001 |
||||||
|
peer_public_key=ce8871f07fa056c636d297115f231b08c29cdf94e0d440fce83a07c34416d36a |
||||||
|
|
||||||
|
[debug] |
||||||
|
connection=info |
||||||
|
socket=info |
||||||
|
general=info |
||||||
|
traffic=info |
||||||
|
etcp_route=debug |
||||||
|
proxy=debug |
||||||
|
|
||||||
Loading…
Reference in new issue