From f003aaa92d4f7383a5d2d9aa1e503d0a48e37217 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 24 Oct 2026 12:36:16 +0200 Subject: [PATCH] docs(proxy): reconcile TCP client server and listener behavior --- src/proxy/socks_proxy_doc.md | 54 +++++-- src/proxy/tcp_proxy_client_doc.md | 253 ++++++++---------------------- src/proxy/tcp_proxy_server_doc.md | 158 ++++++++----------- 3 files changed, 174 insertions(+), 291 deletions(-) diff --git a/src/proxy/socks_proxy_doc.md b/src/proxy/socks_proxy_doc.md index 0ef99ccd..d1f794a4 100644 --- a/src/proxy/socks_proxy_doc.md +++ b/src/proxy/socks_proxy_doc.md @@ -22,10 +22,11 @@ struct listen_ctx* listen = socks_proxy_init_listen( `is_http=0` — SOCKS5, `is_http=1` — HTTP CONNECT/прокси. ### Приём ETCP-данных от exit node -Внешний dispatcher (`tcp_proxy_client`) вызывает `socks_proxy_handle_etcp()` при получении ETCP-пакетов с `svc_id=ETCP_RT_ID_TCP_PROXY`: +Внешний dispatcher (`tcp_proxy_client`) проверяет peer/group и вызывает `socks_proxy_handle_etcp()` +при получении пакетов `ETCP_RT_ID_TCP_PROXY_CLIENT` (0x08). Запросы уходят в `ETCP_RT_ID_TCP_PROXY_SERVER` (0x04): ```c int handled = socks_proxy_handle_etcp(&conns, &conn_count, stream_id, subcmd, data, data_len); -// handled=1 — найден и обработан, 0 — stream_id не найден +// handled=1 — stream_id найден (включая игнорирование команды), 0 — stream_id не найден ``` Поддерживаемые subcmd: `CONNECTED`, `DATA`, `WINDOW`, `CLOSE`, `ERROR`, `FIN`. @@ -37,12 +38,17 @@ struct socks_proxy_conn* c = socks_proxy_find_conn(conns, stream_id); ### Завершение ```c -socks_proxy_conn_free_all(&conns, &conn_count); // все соединения socks_proxy_close_listen(ua, listen, &sock_out); // слушатель +socks_proxy_conn_free_all(&conns, &conn_count); // все соединения ``` +`inst/ua/conns/count/next_stream_id` заимствуются и должны жить до закрытия listener и всех клиентов. +Закрытие listener сохраняет активных клиентов. Вызовы и callbacks выполняются в uasync владельца. +Адрес listener — IPv4:port; порт 0 выбирает ОС, фактический порт возвращает `socks_proxy_get_port()`. ### Ключевые нюансы -- **Бэкпрессур (backpressure):** при неудаче `etcp_route_send()` данные сохраняются в `tx_buf/tx_len`, и регистрируется waiter через `etcp_router_on_send_ready()`. Когда ETCP готов к отправке, `tx_waiter_cb` повторяет попытку и возобновляет `read_queue`. +- **Backpressure:** DATA ждёт CONNECTED и кредита `proxy_flow`. Неотправленные байты сохраняются + в `tx_buf/tx_len` и отправляются порциями до 4096. При доступном окне ставится router send-ready + waiter; safety-net 500 мс возобновляет отправку с обычным force=0. Чтение tcp_io ограничено его очередью. - **Несколько соединений:** управляются через связный список `struct socks_proxy_conn*` + счётчик. В HTTP keep-alive каждое сообщение получает новый upstream stream; клиентский TCP-сокет сохраняется. Pipelining обрабатывается последовательно, включая запросы к разным адресатам. - **stream_id** генерируется монотонно (`++next_stream_id`), передаётся в заголовке ETCP-пакетов для мультиплексирования. @@ -71,31 +77,32 @@ socks_proxy_close_listen(ua, listen, &sock_out); // слушатель | Функция | Описание | |---------|----------| -| `socks_proxy_init_listen(ua, addr, conns, count, next_sid, inst, via_node, is_http)` | Создаёт слушающий TCP-сокет на `addr` (IP:PORT). Возвращает `listen_ctx*`. | +| `socks_proxy_init_listen(ua, addr, conns, count, next_sid, inst, via_node, is_http)` | Создаёт IPv4 listener, возвращает собственный ctx или NULL. Не регистрирует router callback. | | `socks_proxy_close_listen(ua, ctx, sock_out)` | Закрывает слушающий сокет, освобождает ctx. | +| `socks_proxy_get_port(ctx)` | Фактический порт listener, 0 при ошибке. | | `socks_proxy_handle_etcp(head, count, stream_id, subcmd, data, len)` | Диспетчер ETCP-сообщений от exit node. Обрабатывает DATA (→write_queue клиенту), CLOSE/ERROR (→push_close), FIN (→push_fin). | | `socks_proxy_find_conn(head, stream_id)` | Поиск соединения по stream_id в связном списке. | -| `socks_proxy_conn_free(c)` | Освобождает соединение: отправляет отложенный CLOSE/ERROR в ETCP, удаляет из списка, освобождает `tcp_conn`, buffer, waiter. | +| `socks_proxy_conn_free(c)` | Удаляет из списка, отменяет DNS/control retry/DATA timer/waiter, освобождает TCP/HTTP/buffers. Активный HTTP upstream отменяется; pending CLOSE/ERROR получает последнюю попытку. | | `socks_proxy_conn_free_all(head, count)` | Освобождает все соединения в списке. | ### Внутренние функции | Функция | Описание | |---------|----------| -| `send_msg(inst, dst, subcmd, sid, data, len, force)` | Формирует ETCP-пакет: `[TCP_PROXY_HDR_SIZE=6] svc_id(1)+subcmd(1)+stream_id(4)` + payload, отправляет через `etcp_route_send`. | +| `send_msg(inst, group_id, dst, subcmd, sid, data, len, force)` | Через общий proxy_send копирует payload и отправляет в TOPO_GROUP_UTUN; group_id аргумент игнорируется, mode=0. | | `send_connect(c)` | Отправляет CONNECT (dest_ip[4]+dest_port[2]) на exit node. | | `send_data(c, data, len)` | Отправляет DATA (релей пользовательского трафика). | -| `send_close(c)`, `send_error(c)`, `send_fin(c)` | Отправляют управляющие subcmd. | +| `send_close(c)` | Попытка CLOSE с force=1; pending повторяется при free. ERROR отправляется через send_msg, FIN — через proxy_flow_flush. | | `write_to_client(c, data, len)` | Запись данных в `tc->write_queue` (отправка клиенту через TCP). | | `process_socks_greeting(c)` | Парсинг SOCKS5 GREETING: ver=5, выбор метода 0x00 (no auth), ответ 05 00. | -| `process_socks_request(c)` | Парсинг SOCKS5 CONNECT REQUEST (atyp=1/3/4, IPv4/IPv6/domain). DNS-резолвинг для доменов. Ответ 05 00 00 01 + dummy addr. | +| `process_socks_request(c)` | CONNECT к IPv4 или DNS-имени; IPv6 отклоняется с REP=8. Успешный ответ отправляет socks_connected только после CONNECTED. | | `http_read(c)`, `http_receive(c)` | Потоковый разбор клиентского запроса и upstream ответа через HTTP codec. | | `http_finish(c)` | Завершение/отмена upstream, ожидание доставки ответа клиенту и переход к следующему запросу. | | `http_fail(c)`, `http_deadline(c)` | Ошибки и тайм-ауты: собственный HTTP error до final headers, закрытие оборванного сообщения после них. | | `on_accept_cb(sock, arg)` | Callback accept: создаёт `socks_proxy_conn`, `tcp_conn_create`, вешает read callback. | | `on_read_cb(q, arg)` | Callback чтения из TCP: handshake-парсинг или релей данных через ETCP с бэкпрессуром. | | `tx_waiter_cb(q, arg)` | Callback бэкпрессура: повторная отправка отложенных данных при готовности ETCP. | -| `on_fin_cb`, `on_error_cb`, `on_closed_cb` | Callback'и жизненного цикла `tcp_conn`: FIN→удалённый FIN, ошибка→send_error+free, закрыт→send_close+free. | +| `on_fin_cb`, `on_error_cb`, `on_closed_cb` | FIN ждёт исходящих DATA; ошибка отправляет ERROR, cleanup из TCP callbacks откладывается через call_soon. HTTP вне туннеля использует границы транзакций. | ## 4. Протокол @@ -104,8 +111,9 @@ socks_proxy_close_listen(ua, listen, &sock_out); // слушатель Клиент → uTun: [ver=5][nmethods][methods...] uTun → Клиент: [05][00] (GREETING: no auth) Клиент → uTun: [05][01][00][atyp][addr...][port] (CONNECT REQUEST) -uTun → Клиент: [05][00][00][01][0.0.0.0][0] (ответ ок) uTun → exit (ETCP): TCP_PROXY CONNECT[dest_ip+port] +exit → uTun (ETCP): CONNECTED (реальный connect завершён) +uTun → Клиент: [05][00][00][01][0.0.0.0][0] (ответ ок) Клиент ↔ uTun ↔ exit: RELAY данных через ETCP DATA ``` @@ -121,10 +129,12 @@ uTun → Клиент: HTTP/1.1 200 Connection Established\r\n\r\n ### HTTP-прокси (GET/POST, is_http=1) ``` Клиент → uTun: GET http://host:port/path HTTP/1.1\r\nHost: ...\r\n\r\n -uTun → exit (ETCP): TCP_PROXY CONNECT[dest_ip+port] + TCP_PROXY DATA[GET /path HTTP/1.1\r\n...] +uTun → exit (ETCP): TCP_PROXY CONNECT[dest_ip+port] +exit → uTun (ETCP): CONNECTED +uTun → exit (ETCP): TCP_PROXY DATA[GET /path HTTP/1.1\r\n...] exit → uTun: HTTP headers/body через ETCP DATA, затем FIN uTun → Клиент: проверенный и реконструированный HTTP response -uTun → exit (ETCP): CLOSE завершённого stream +uTun → exit (ETCP): CLOSE завершённого stream (после полного ответа, FIN нужен для тела до EOF) Клиент → uTun: следующий запрос → новый CONNECT с новым stream_id ``` URL преобразуется в origin-form без потери query; Host строится из absolute URL. Заголовки текущего соединения, включая поля из Connection, и Proxy-Authorization удаляются в обоих направлениях. Framing и Connection формируются заново, добавляется Via. Upstream обычного запроса получает Connection: close; frontend HTTP/1.1 сохраняет keep-alive для ответов с определённой длиной. @@ -139,12 +149,26 @@ ETCP credit возвращается за исходные принятые ба ERROR на exit немедленно уничтожает сокет и отложенный upload; CLOSE сохраняет обычное завершение очереди. +SOCKS greeting принимает только предложенный метод 0 (no auth); иначе отправляется 05 FF. +Реализован только CONNECT, без BIND/UDP ASSOCIATE. DNS выполняется на ingress через +асинхронный A-resolver, выбирается первый адрес. Хвост совмещённых greeting/request/data сохраняется. +Ошибка DNS/connect до установления потока даёт SOCKS REP=4 либо HTTP 502. + +В сыром туннеле FIN локального клиента ждёт tx_buf и read_queue, но не write_queue +противоположного направления. FIN от exit ставится в write_queue клиента после предыдущих DATA. +После обоих FIN и фактического локального shutdown ставится close-маркер. WINDOW возвращает +кредит после опустошения write_queue и write_buf. Общий retry повторяет CONNECTED/WINDOW/FIN, +отдельной гарантированной очереди CLOSE/ERROR у listener нет. + ## 5. Зависимости -- `tcp_proxy_server.h` — константы subcmd (`TCP_PROXY_SUBCMD_*`) и размер заголовка (`TCP_PROXY_HDR_SIZE=6`) -- `etcp.h` / `etcp_api.h` — `etcp_route_send()`, `ETCP_RT_ID_TCP_PROXY` +- `proxy_protocol.h` — subcmd, размеры заголовков, proxy_flow и proxy_send +- `etcp_api.h` — `ETCP_RT_ID_TCP_PROXY_SERVER/CLIENT` - `etcp_router.h` — `etcp_router_on_send_ready()`, `etcp_router_cancel_send_ready()` - `tcp_io.h` / `tcp_conn` — TCP-соединения (`tcp_conn_create`, `tcp_conn_push_close`, `tcp_conn_push_fin`, write/read очереди) - `u_async.h` — event loop, `uasync_add_socket_t` - `ll_queue.h` — очереди (write_queue, read_queue), waiter механизм - `memory_pool.h` — аллокация из пулов (`entry_pool`, `data_pool`) - `utun_instance.h` — `struct UTUN_INSTANCE` + +Нормативные размеры сообщений и порядок байтов — в [proxy_protocol.md](../../doc/proxy_protocol.md). +Контракты чистого HTTP codec (обнуление состояния, emit/source_len, used и EOF) — в [http_proxy.h](http_proxy.h). diff --git a/src/proxy/tcp_proxy_client_doc.md b/src/proxy/tcp_proxy_client_doc.md index caef21de..90f5805d 100644 --- a/src/proxy/tcp_proxy_client_doc.md +++ b/src/proxy/tcp_proxy_client_doc.md @@ -1,204 +1,87 @@ -# tcp_proxy_client +# TCP proxy client -## 1. Назначение +`tcp_proxy_client.c/h` объединяет TUN/lwIP, SOCKS5 CONNECT и HTTP forward proxy/CONNECT, +передавая потоки к общему exit `via_node_id`. Форматы и общие правила — +[doc/proxy_protocol.md](../../doc/proxy_protocol.md), контракт API и полей — +[tcp_proxy_client.h](tcp_proxy_client.h), локальные listeners — +[socks_proxy_doc.md](socks_proxy_doc.md). -Клиентский оркестратор TCP прокси. Создаёт TUN-интерфейс, запускает встроенный lwIP TCP стек, перехватывает исходящие TCP-соединения со стороны клиента и туннелирует их через ETCP к удалённому exit-узлу (tcp_proxy_server). Поддерживает три режима перехвата трафика одновременно: +## Создание и владение -1. **Прозрачное проксирование через TUN** — весь TCP-трафик с TUN-интерфейса попадает в lwIP, который управляет реальными TCP-сессиями. Исходящие соединения прозрачно проксируются на exit-узел. UDP (через `udp_proxy`) и ICMP Echo (через `icmp_proxy`) также пробрасываются. -2. **SOCKS5 proxy listener** — встроенный SOCKS5-сервер, принимающий локальные подключения и туннелирующий их через ETCP. -3. **HTTP CONNECT proxy listener** — встроенный HTTP CONNECT-прокси (работает через тот же механизм SOCKS, что и SOCKS5). +`tcp_proxy_client_create` вызывается в uasync с живым `ua`. TUN/lwIP создаются только +при **обоих** непустых `tun_name` и `tun_ip`. В этом случае `tun_init_nat` создаёт +отдельный proxy TUN (test_mode выбирает тестовый интерфейс); MTU<=0 заменяется на 1500, +MTU TUN передаётся в lwIP. Имя tun_init_nat не означает включение eim_nat на ingress. +Без TUN необходим хотя бы флаг SOCKS или HTTP; эти listeners могут работать без lwIP. -Модуль полностью оркестрирует весь TCP-прокси-пайплайн на стороне клиента: захват трафика → lwIP TCP стек → ETCP-туннелирование → приём ответных данных → отдача в lwIP → запись IP-пакетов обратно в TUN. +`inst/ua/mappings` заимствуются до destroy; результат принадлежит вызывающему коду. +Создание само не заполняет `inst->tcp_proxy_client`: это делает владелец, например +`utun_instance`. Для автономного TUN smoke test допускается inst=NULL, но для +обмена через router нужен instance. Mappings содержат порты в host byte order, +адрес назначения — IPv4-строку. -## 2. Как пользоваться +Критическая ошибка TUN/lwIP/пула возвращает NULL. Ошибка отдельного listener или +router bind не отменяет создание всего клиента; проверять конкретные listener handles +и диагностику. UDP/ICMP инициализируются только для TUN после успешного TCP bind. -### Типовой сценарий +Клиент привязывается к `ETCP_RT_ID_TCP_PROXY_CLIENT` (0x08), сервер — к +`ETCP_RT_ID_TCP_PROXY_SERVER` (0x04). Генератор stream_id общий для TUN/SOCKS/HTTP, +изначально равен 1, перед выдачей инкрементируется. На exit ключ потока — пара peer/sid. -``` -tcp_proxy_client_create(inst, ua, tun_name, tun_ip, mtu, test_mode, - mappings, mapping_count, via_node_id, - socks_enabled, socks_addr, http_proxy_enabled, http_proxy_addr) -→ TUN init (tun_init_nat) — создание TUN-интерфейса с NAT -→ lwIP init (lwip_tcp_init) — запуск встроенного TCP стека -→ Настройка port mappings — прослушивание локальных портов в lwIP -→ SOCKS5/HTTP listen — запуск локальных прокси-слушателей -→ ETCP router bind — регистрация коллбэка tcp_proxy_client_router_recv_cb -→ UDP/ICMP proxy init -``` +## TUN → exit -### Ключевые концепции +Output queue TUN содержит однобайтовый префикс перед IP-пакетом. Обработчик проверяет +IPv4 version/IHL/total length и отвергает фрагменты (MF/offset) с WARN. +Для UDP и ICMP Echo type 8 учитывается IHL, затем вызывается соответствующий proxy. +TCP без IP-заголовка передаётся в `lwip_tcp_input`; перед этим по порту назначения +при необходимости создаётся динамический listen PCB. -**TUN interface** через `tun_init_nat()`. IP-пакеты из output_queue TUN направляются в `tcp_proxy_client_tun_input()` — главную точку входа inbound-трафика. +При accept выбирается первый mapping с совпадающим локальным портом; если его нет — +исходные `pcb->local_ip/local_port`. Создаются `tcp_proxy_client_conn`, `proxy_flow` +и очереди `tx_queue` (lwIP→exit) и `to_lwip` (exit→lwIP), отправляется CONNECT. +Локальный TCP handshake завершает lwIP; его успех не подтверждает connect назначения. +DATA ожидает CONNECTED от exit. -**Встроенный lwIP TCP стек** (`lwip_tcp_init()`) с output-коллбэком `tcp_proxy_client_output_cb`, который пишет готовые IP-пакеты обратно в TUN (в сторону ОС клиента). +Recv callback копирует pbuf в блоки до 4096 байт в tx_queue. Drain отправляет через +`proxy_flow_send`; `tcp_recved` возвращает окно локальному клиенту только после +успешного принятия DATA router. При -2 ожидаются CONNECTED/WINDOW; при другой ошибке +регистрируется router send-ready waiter. Safety-net каждые 500 мс повторяет отправку +с force=1, сохраняя ограничение окна proxy. Неотправленный entry возвращается в начало очереди. -**Inbound TUN трафик** (`tcp_proxy_client_tun_input()`): парсинг IP-пакета → по протоколу: -- TCP → вызов `lwip_tcp_input()` — пакет попадает в lwIP, где срабатывает `accept_cb` для новых соединений или `recv_cb` для существующих -- UDP → `udp_proxy_send_to_exit()` — проброс UDP в exit -- ICMP (type 8/Echo) → `icmp_proxy_send_to_exit()` — проброс ping в exit +## Exit → TUN -**Connection accept** (`tcp_proxy_client_accept_cb`): при новом входящем TCP-соединении в lwIP: -1. Выделяется `struct tcp_proxy_client_conn` -2. Определяется destination: поиск по port mapping config `mappings[]` (local_port → remote_ip:remote_port), иначе `local_ip:local_port` самого pcb -3. Назначается `stream_id` (инкремент `next_stream_id`) -4. Регистрируются lwIP коллбэки: `recv`, `sent`, `err`, `poll` -5. Создаётся очередь `to_lwip` для данных от exit -6. Отправляется ETCP CONNECT с dest_ip+dest_port через `etcp_route_send(force=1)` +Router callback проверяет peer==via_node_id и group==TOPO_GROUP_UTUN, включая пустые +уведомления router. Сначала предлагает stream SOCKS/HTTP спискам, затем ищет lwIP-поток. -**Client-side lwIP** — чтение данных (`tcp_proxy_client_recv_cb`): -- При получении данных от lwIP (клиент что-то отправляет): копируем pbuf → `tcp_proxy_client_send_data()` → ETCP DATA -- При backpressure (router send_q переполнен): сохраняем в `tx_buf`, регистрируем waiter через `etcp_router_on_send_ready()`. При освобождении очереди `tcp_proxy_client_tx_waiter_cb` досылает данные и вызывает `tcp_recved()` для window update -- При p==NULL (FIN от lwIP): выставляется `fin_local`, отправляется ETCP FIN. Если обе стороны закрылись — ETCP CLOSE +CONNECTED открывает `flow.ready`; WINDOW проверяет и увеличивает кредит, возобновляя +очереди. DATA после проверки окна помещается в to_lwip. `feed_from_transport` пишет +часть entry по доступному `tcp_sndbuf` с `TCP_WRITE_FLAG_COPY`, оставляя хвост в очереди. +Кредит возвращается после успешного `tcp_write`, затем вызывается `tcp_output`. +Дальнейший feed запускают DATA, WINDOW/wake, sent и poll callbacks. +Выходные IPv4-пакеты lwIP записываются через `tun_write`. -**Exit-side data** — приём ответных данных (`tcp_proxy_client_handle_data()`): -- Данные от exit помещаются в очередь `to_lwip` -- `tcp_proxy_client_feed_from_transport()`: пока есть место в `tcp_sndbuf(pcb)`, читает из `to_lwip` и пишет через `tcp_write()` в lwIP. После записи вызывает `tcp_output()` для отправки -- Trigger на feed: при получении DATA, при `sent_cb` (lwIP освободил send buffer) +## Завершение -**Full lifecycle:** -1. CONNECT — установка соединения с exit -2. DATA ↔ DATA — двунаправленный обмен данными -3. FIN (от клиента или exit) — half-close: `tcp_shutdown(pcb, 0, 1)` (закрытие только send-стороны) -4. CLOSE — полное закрытие после обоих FIN, очистка `tcp_proxy_client_conn_free()` -5. ERROR — аварийное закрытие (ошибка lwIP или exit), немедленная очистка +Локальный FIN устанавливает fin_local. FIN к exit ставится только после опустошения +tx_queue; общий control retry ждёт ready и повторяет неудавшуюся отправку. +Удалённый FIN устанавливает fin_remote/flow.fin_received и fin_deferred. +После дренажа to_lwip выполняется `tcp_shutdown(pcb, 0, 1)`; при неудаче попытка повторяется. -**Per-port mapping конфигурация** (`tcp_proxy_client_mapping_config`): -```c -struct tcp_proxy_client_mapping_config { - uint16_t local_port; // локальный порт для прослушивания - char remote_ip[64]; // IP назначения на exit-узле - uint16_t remote_port; // порт назначения на exit-узле -}; -``` -При старте для каждого mapping создаётся listen pcb в lwIP. Также работает динамический listen для любых исходящих портов (`tcp_proxy_client_ensure_outbound_listen()`). +Когда обе половины завершены, PCB отсоединяется от callbacks: lwIP может самостоятельно +освободить его по TIME_WAIT без err callback. Поток удаляется после отправленного FIN, +полученного FIN, снятого fin_deferred и пустых tx_queue/to_lwip. Штатное завершение +не требует дополнительного обмена CLOSE. -**SOCKS5/HTTP proxy listeners** — используют `socks_proxy_init_listen()` (общий код с SOCKS, флаг `is_http` различает режимы). SOCKS/HTTP обрабатываются приоритетно в `router_recv_cb` — им не нужен TUN/lwIP. +Входящий CLOSE прекращает поток через `tcp_close` и очистку, ERROR вызывает abort. +Ошибка lwIP обнуляет PCB, который уже освобождает стек; остальная очистка отменяет +control retry, DATA timer, router waiter и освобождает обе очереди. +CLOSE/ERROR при неудаче отправки могут повторяться через close_pending в poll/free +пока поток жив; отдельной сохраняемой очереди этих команд нет. -### Протокол обмена с exit-узлом +Пустое уведомление router (`ROUTER_SVC_HDR_SIZE=26`, без TCP command/sid) удаляет +все lwIP/SOCKS/HTTP потоки клиента к настроенному exit. Это не 9-байтовый wire CLOSE_ALL. -Формат сообщения: `svc_id(1) + subcmd(1) + stream_id(4) + [data]` (заголовок `TCP_PROXY_HDR_SIZE = 6` байт). - -| Subcmd | Код | Направление | Описание | -|--------|-----|-------------|----------| -| CONNECT | 0x01 | client→exit | Установка TCP-соединения с destination | -| DATA | 0x03 | ↔ | Данные TCP-потока | -| CLOSE | 0x04 | ↔ | Полное закрытие потока | -| ERROR | 0x05 | ↔ | Ошибка, немедленная очистка | -| FIN | 0x06 | ↔ | Half-close (shutdown write) | - -Специальное сообщение: `svc_id(1) + peer_id(8)` (9 байт) — CLOSE_ALL: очистка всех соединений к указанному peer. - -### Механизмы надёжности - -- **Backpressure на router send_q**: `etcp_router_on_send_ready()` — если `etcp_route_send()` не может отправить (inflight полон), данные буферизуются в `tx_buf` и досылаются при освобождении -- **Повтор CLOSE/ERROR**: если отправка CLOSE/ERROR не удалась, выставляется `close_pending=1`. В `poll_cb` повторяется попытка -- **Очередь to_lwip**: буферизует данные от exit до готовности lwIP send buffer, обеспечивая window-based flow control - -## 3. API - -### Структуры - -#### `struct tcp_proxy_client_conn` — одно проксируемое TCP-соединение -| Поле | Тип | Назначение | -|------|-----|------------| -| `next` | `*` | Связный список всех соединений | -| `proxy` | `*tcp_proxy_client` | Обратный указатель на родительский прокси | -| `pcb` | `*tcp_pcb` | lwIP protocol control block для этого соединения | -| `stream_id` | `uint32_t` | Уникальный идентификатор потока в протоколе ETCP | -| `to_lwip` | `*ll_queue` | Очередь данных от exit → lwIP (flow control) | -| `fin_local` | `uint8_t` | Локальная сторона (lwIP) отправила FIN | -| `fin_remote` | `uint8_t` | Удалённая сторона (exit) отправила FIN | -| `rem_closed` | `uint8_t` | Exit прислал CLOSE — прекращаем обработку | -| `error` | `uint8_t` | Ошибка на любом конце — немедленная очистка | -| `close_sent` | `uint8_t` | CLOSE/ERROR отправлен на exit | -| `close_pending` | `uint8_t` | CLOSE/ERROR не доставлен, ждём повтора в poll | -| `tx_buf` | `*uint8_t` | Буфер при backpressure (lwIP→ETCP send fail) | -| `tx_len` | `uint16_t` | Длина данных в tx_buf | -| `tx_waiter` | `queue_waiter_handle` | Дескриптор waiter для backpressure | -| `dest_ip` | `uint8_t[4]` | IP назначения на exit-узле | -| `dest_port` | `uint16_t` | Порт назначения на exit-узле (network byte order) | - -#### `struct tcp_proxy_client` — состояние прокси-клиента -| Поле | Тип | Назначение | -|------|-----|------------| -| `inst` | `*UTUN_INSTANCE` | Родительский инстанс uTun | -| `ua` | `*UASYNC` | Асинхронный event loop | -| `tun` | `*tun_if` | TUN-интерфейс для захвата/инжекции трафика | -| `conns` | `*tcp_proxy_client_conn` | Голова связного списка lwIP-соединений | -| `conn_count` | `int` | Количество активных lwIP-соединений | -| `entry_pool` | `*memory_pool` | Пул для `struct ll_entry` (данные to_lwip) | -| `next_stream_id` | `uint32_t` | Счётчик stream_id (начинается с 1) | -| `via_node_id` | `uint64_t` | Node ID exit-узла для маршрутизации ETCP | -| `lwip` | `*lwip_tcp_ctx` | Контекст встроенного lwIP TCP стека | -| `mappings` | `*mapping_config` | Указатель на массив port mapping конфигурации | -| `mapping_count` | `int` | Количество mapping записей | -| `socks_enabled` | `int` | Флаг включения SOCKS5 прокси | -| `socks_conns` | `*socks_proxy_conn` | Список активных SOCKS5-соединений | -| `socks_conn_count` | `int` | Количество SOCKS5-соединений | -| `socks_listen` | `*listen_ctx` | Слушающий сокет SOCKS5 | -| `http_proxy_enabled` | `int` | Флаг включения HTTP CONNECT прокси | -| `http_conns` | `*socks_proxy_conn` | Список активных HTTP CONNECT соединений | -| `http_conn_count` | `int` | Количество HTTP CONNECT соединений | -| `http_listen` | `*listen_ctx` | Слушающий сокет HTTP CONNECT | - -### Функции - -#### `tcp_proxy_client_create()` -Создаёт и инициализирует весь прокси-клиент. Параметры: инстанс uTun, uasync, имя/IP/MTU TUN-интерфейса, test_mode, массив port mapping, via_node_id, флаги/адреса SOCKS и HTTP прокси. Возвращает NULL при ошибке. - -#### `tcp_proxy_client_destroy()` -Полная очистка: отвязка от ETCP router, остановка UDP/ICMP прокси, закрытие SOCKS/HTTP listeners, освобождение всех соединений (lwIP abort + очистка очередей to_lwip + tx_buf + waiter cancel), уничтожение lwIP стека, закрытие TUN, освобождение memory pool. - -#### `tcp_proxy_client_router_recv_cb()` -Главный ETCP-коллбэк (зарегистрирован через `etcp_router_bind` на `ETCP_RT_ID_TCP_PROXY=0x04`). Обрабатывает входящие сообщения от exit-узла: -- **DATA** → `tcp_proxy_client_handle_data()` — данные в очередь to_lwip и feed в lwIP -- **CLOSE** → `tcp_proxy_client_handle_close()` — пометка rem_closed, закрытие lwIP pcb, освобождение соединения -- **ERROR** → `tcp_proxy_client_handle_error()` — аварийное закрытие lwIP pcb, освобождение соединения -- **FIN** → `tcp_proxy_client_handle_fin()` — half-close через tcp_shutdown(pcb, 0, 1) -- **CLOSE_ALL** (9-байтовый пакет без conn) → очистка всех соединений к peer -- Сначала пробует SOCKS/HTTP обработчики (приоритет — могут работать без TUN) - -### Внутренние функции - -#### lwIP коллбэки -- **`tcp_proxy_client_output_cb()`** — output lwIP: запись IP-пакетов в TUN (в сторону ОС клиента) -- **`tcp_proxy_client_accept_cb()`** — новый входящий TCP: создание `tcp_proxy_client_conn`, определение destination из mapping конфига, отправка ETCP CONNECT -- **`tcp_proxy_client_recv_cb()`** — данные от клиента (lwIP→ETCP): копирование pbuf → ETCP DATA с backpressure -- **`tcp_proxy_client_sent_cb()`** — lwIP освободил send buffer: вызов feed_from_transport для передачи накопленных данных -- **`tcp_proxy_client_err_cb()`** — ошибка lwIP: pcb уже уничтожен стеком, отправка ETCP ERROR -- **`tcp_proxy_client_poll_cb()`** — периодический poll (2 интервала): очистка при error, повтор close_pending - -#### Отправка сообщений в ETCP -- **`tcp_proxy_client_send_msg()`** — базовая функция: сборка пакета `svc_id+subcmd+stream_id+data`, отправка через `etcp_route_send()` -- **`tcp_proxy_client_send_connect()`** — CONNECT с dest_ip+dest_port (6 байт данных), force=1 -- **`tcp_proxy_client_send_data()`** — DATA с payload, без force (backpressure через waiter) -- **`tcp_proxy_client_send_close()`** — CLOSE, force=1, при ошибке выставляет close_pending -- **`tcp_proxy_client_send_error()`** — ERROR, force=1, при ошибке выставляет close_pending -- **`tcp_proxy_client_send_fin()`** — FIN, force=1 - -#### Обработка трафика -- **`tcp_proxy_client_tun_input()`** — коллбэк очереди output_queue TUN. Парсит IP-пакеты: TCP → lwip_tcp_input (с динамическим listen на целевой порт), UDP/ICMP → соответствующие прокси -- **`tcp_proxy_client_handle_non_tcp()`** — перенаправление UDP в `udp_proxy_send_to_exit()` и ICMP Echo в `icmp_proxy_send_to_exit()` -- **`tcp_proxy_client_feed_from_transport()`** — чтение данных из очереди `to_lwip` и запись в lwIP через `tcp_write()` пока есть место в send buffer. После feed вызывает `tcp_output()` -- **`tcp_proxy_client_tx_waiter_cb()`** — коллбэк backpressure waiter: досылка `tx_buf` при освобождении router send_q, вызов `tcp_recved()` для window update -- **`tcp_proxy_client_ensure_outbound_listen()`** — динамическое создание listen pcb в lwIP для заданного порта (если ещё не создан), нужно для прозрачного проксирования любых исходящих TCP-соединений - -#### Управление памятью и соединениями -- **`tcp_proxy_client_entry_from_data()`** — создание `ll_entry` из пула `entry_pool` с копированием данных -- **`tcp_proxy_client_find_conn()`** — линейный поиск соединения по `stream_id` -- **`tcp_proxy_client_conn_free()`** — полная очистка одного соединения: досылка pending CLOSE/ERROR, удаление из списка, очистка очереди to_lwip, освобождение tx_buf, cancel waiter, free памяти - -### Зависимости -- `tun_if.h` — TUN-интерфейс (инициализация, запись, закрытие) -- `lwip_tcp/lwip_tcp.h` — встроенный lwIP TCP стек (инициализация, input, управление pcb) -- `etcp.h` / `etcp_api.h` / `etcp_router.h` — ETCP-маршрутизация и отправка данных -- `tcp_proxy_server.h` — общие константы протокола (TCP_PROXY_SUBCMD_*, TCP_PROXY_HDR_SIZE) -- `socks_proxy.h` — SOCKS5/HTTP CONNECT прокси (init_listen, handle_etcp, conn_free_all) -- `udp_proxy.h` / `icmp_proxy.h` — проброс UDP и ICMP -- `config_parser.h` — `tcp_proxy_client_mapping_config` (local_port → remote_ip:remote_port) -- `utun_instance.h` — `struct UTUN_INSTANCE` с полем `tcp_proxy_client` -- `lib/u_async.h` — асинхронный event loop -- `lib/ll_queue.h` — lock-free очереди (to_lwip, output_queue TUN) -- `lib/memory_pool.h` — пул для `struct ll_entry` -- `lib/mem.h` — обёртки malloc/free с учётом утечек -- `lib/debug_config.h` — логирование +`destroy` снимает TCP binding, закрывает listeners/клиентов, освобождает lwIP/TUN/пул. +Общие UDP/ICMP уничтожаются только при выключенном сервере того же instance. +Владелец затем сам обнуляет `inst->tcp_proxy_client`. Все callbacks и очистка выполняются +в uasync; уничтожать клиента следует вне его активного callback. diff --git a/src/proxy/tcp_proxy_server_doc.md b/src/proxy/tcp_proxy_server_doc.md index 435ee9a4..6c15fa94 100644 --- a/src/proxy/tcp_proxy_server_doc.md +++ b/src/proxy/tcp_proxy_server_doc.md @@ -1,109 +1,85 @@ -# tcp_proxy_server +# TCP proxy server (exit) -## 1. Назначение +`tcp_proxy_server.c/h` открывает обычный TCP к IPv4-назначению по CONNECT ingress-узла +и передаёт данные через UTUN-router. Общий протокол — +[doc/proxy_protocol.md](../../doc/proxy_protocol.md), контракт API — +[tcp_proxy_server.h](tcp_proxy_server.h). -TCP прокси-сервер на exit node. Принимает CONNECT-запросы от клиента через ETCP, создаёт OS TCP сокет к реальному адресату, релеит данные в обе стороны: клиент↔ETCP↔сервер↔OS TCP↔destination. Поддерживает backpressure (пауза чтения из OS сокета при заполнении ETCP очереди), graceful CLOSE, экспоненциальный ретрай управляющих сообщений, диагностические таймеры. +## Инициализация и ресурсы -Работает в паре с `tcp_proxy_client` (на ingress node) и `lwip_tcp` (lwIP-стек для обработки TCP-сессий). +Контекст встроен в `inst->tcp_proxy_server`. `init(inst)` обнуляет его, читает +`config->global.tcp_proxy_server_enabled`, при включении регистрирует сервис +`ETCP_RT_ID_TCP_PROXY_SERVER` (0x04) и вызывает UDP/ICMP init независимо от наличия клиента. +NULL inst даёт -1; отсутствие config или отключённый сервер дают 0 без binding. +Результаты bind/UDP/ICMP init не пробрасываются: 0 не гарантирует готовность всех сервисов. +Повторный init при живых потоках недопустим. -При старте сервера автоматически инициализируются `udp_proxy` и `icmp_proxy`, если клиентский TCP прокси не активен (чтобы exit node обслуживал все виды трафика). +INI-парсер включает сервер при обработке параметра секции `[tcp_proxy_server]`. +Поддержанные ключи: `tcp_recv_buf`, `fib`, `so_mark`, `netif`, `bind_ip`, +`connect_timeout`. Ключ `enabled` в этой секции текущим парсером не поддерживается; +для включения достаточно, например, `connect_timeout=2000`. +Пустая секция без параметров сама по себе флаг не выставляет. -## 2. Как пользоваться +Каждый поток принадлежит серверу, содержит собственный `tcp_conn` и `proxy_flow`. +Поиск выполняется по паре `peer_node_id/stream_id`; одинаковые sid разных пиров допустимы. +Повторный CONNECT существующей пары отклоняется с ERROR. -### Включение -В конфиге: `tcp_proxy_server_enabled = yes` — сервер регистрирует обработчик `ETCP_RT_ID_TCP_PROXY` через `etcp_router_bind()` и готов принимать команды от клиентов. +## Connect и обмен данными -### Типовой сценарий -1. **CONNECT (0x01):** клиент шлёт `dest_ip(4) + dest_port(2)`, сервер создаёт `tcp_proxy_server_conn`, открывает OS TCP сокет, делает `connect()` к destination. -2. **DATA (0x03):** клиент шлёт данные → сервер пишет в OS write_queue через pool-аллокацию. -3. **Чтение из destination:** данные с OS сокета попадают в `read_queue`, оттуда `read_queue_drain_cb` релеит их клиенту через ETCP. -4. **CLOSE (0x04):** клиент закрыл соединение → сервер паузит чтение, сливает read_queue, шлёт shutdown(SHUT_WR) в OS сокет. -5. **FIN (0x06):** клиент подтверждает получение FIN от destination → сервер push-ит FIN в OS сокет (завершение полного закрытия). -6. **ERROR (0x05):** ошибка на клиенте → сервер форсированно закрывает соединение. -7. **Очистка:** по `on_closed_cb` (OS сокет закрыт полностью) → `tcp_proxy_server_conn_free()` освобождает все ресурсы. +CONNECT содержит IPv4 и порт в сетевом порядке. Создаётся неблокирующий OS-сокет, +применяются bind_ip, SO_MARK, FreeBSD FIB и привязка к интерфейсу там, где поддержаны, +затем создаётся tcp_conn и вызывается connect. Эти настройки относятся к TCP-сокетам, +UDP/ICMP их не наследуют. Таймаут connect — `connect_timeout` мс, +по умолчанию 2000; 0 отключает таймаут. Асинхронная ошибка/таймаут приводит к ERROR и очистке. -### Ключевые нюансы -- **Backpressure:** если `etcp_route_send()` вернула ошибку — данные буферизуются в `tx_buf`, регистрируется `pause_waiter` (ожидание освобождения ETCP канала), запускается fallback `retry_timer` (500ms, force=1). При возобновлении — сначала делается повтор tx_buf, потом продолжается дрейн очереди. -- **FIN handling:** FIN от destination не отправляется клиенту пока есть pending writes в OS сокет (ждём `on_flushed_cb`) или pending reads (tx_buf / read_queue не пусты — флаг `dst_fin_deferred`). При опустошении очередей — FIN отправляется немедленно. -- **Graceful CLOSE:** при получении CLOSE от клиента чтение из OS сокета паузится через `tcp_conn_pause_read()`, read_queue сливается (данные отбрасываются), push-ится close в OS сокет. -- **Ретрай CLOSE/ERROR:** при неудачной отправке `send_msg()` — экспоненциальный backoff (50→100→200→…→5000 tb = 5ms→500ms) через `close_timer` + `close_retry_cb`. -- **Диагностический таймер (1s):** логирует TCP буферы ОС, размеры read/write очередей, free counts entry/data pool-ов. -- **Connect-таймаут:** `connect()` к destination асинхронный; если за `connect_timeout` мс (конфиг `[tcp_proxy_server]`, по умолчанию 2000, `0` = без таймаута) соединение не установилось — срабатывает `tcp_conn_handle_error(ETIMEDOUT)` → клиенту уходит ERROR, соединение освобождается (защита от «чёрной дыры» и утечки fd). -- **CLOSE_ALL:** специальный 9-байтовый пакет без валидного conn удаляет все соединения для заданного peer. +Реальный успех connect ставит pending CONNECTED. Только после принятия CONNECTED router +flow становится ready. Сервер отправляет ответы в `ETCP_RT_ID_TCP_PROXY_CLIENT` (0x08). +Все proxy send используют `TOPO_GROUP_UTUN` и mode=0. -## 3. API +DATA клиента проверяется через proxy_flow_receive, копируется в pool-backed write_queue TCP. +Кредит WINDOW возвращается после опустошения **и write_queue, и write_buf**, +то есть после передачи принятой порции в OS TCP, а не при получении ETCP-пакета. +Счётчик bytes_sent увеличивается до проверки/постановки в очередь и не является +числом реально доставленных destination байтов. -### Структуры +Данные destination берутся из read_queue и отправляются через proxy_flow_send (force=0). +При блокировке entry возвращается в начало read_queue. Для ошибки router ставится +send-ready waiter; отсутствие ready/кредита (-2) ждёт CONNECTED/WINDOW. +Safety-net 500 мс возобновляет drain с обычным force=0, не обходит окно/очередь router. +Чтение tcp_io ограничивается его очередью. Отдельных tx_buf/pause_buf у сервера нет. -| Структура | Назначение | -|-----------|-----------| -| `tcp_proxy_server` | Корневой контекст: enabled, связный список conns, conn_count, ссылка на UTUN_INSTANCE | -| `tcp_proxy_server_conn` | Состояние одного прокси-соединения: stream_id, peer_node_id, dest_ip:port, tcp_conn, буферы/счётчики/таймеры для backpressure и закрытия | +## FIN, CLOSE и ERROR -### Функции инициализации/завершения +FIN destination отправляется клиенту только после опустошения read_queue. +Ожидание write_queue противоположного направления для этого не требуется. +FIN клиента отмечает flow.fin_received и ставит FIN-маркер в OS write_queue: +shutdown(SHUT_WR) следует за всеми ранее принятыми DATA. Дальнейшие DATA после FIN отклоняются. +Когда FIN отправлен, принят встречный FIN и OS TCP отправил локальный FIN, +в write_queue ставится close-маркер. -| Функция | Описание | -|---------|----------| -| `tcp_proxy_server_init(inst)` | Регистрирует ETCP биндинг на `ETCP_RT_ID_TCP_PROXY`, авто-инициализирует UDP/ICMP прокси если tcp_proxy_client не активен | -| `tcp_proxy_server_destroy(inst)` | Освобождает все соединения, уничтожает UDP/ICMP прокси | +CLOSE клиента устанавливает cli_closed, паузит OS-чтение и отбрасывает read_queue. +При установленном TCP close-маркер ждёт ранее принятые записи write_queue; +ещё не подключённый поток освобождается сразу. ERROR немедленно уничтожает поток, +не ожидая дренажа потенциально зависшего upload. -### Обработчики команд (вызываются из `tcp_proxy_server_recv_cb`) +Общий proxy_flow повторяет только pending CONNECTED/WINDOW/FIN через 500 мс. +CLOSE/ERROR не имеют экспоненциального backoff или собственной очереди server retry. +OS error callback удаляет поток и делает отдельную попытку отправки ERROR; +conn_free сам по себе не отправляет control-сообщение. -| Функция | Описание | -|---------|----------| -| `tcp_proxy_server_recv_cb(conn, entry)` | Главный ETCP-коллбэк: диспетчеризует subcmd → handle_connect/data/close/error/fin, обрабатывает CLOSE_ALL | -| `tcp_proxy_server_handle_connect(inst,entry,sid,src)` | Создаёт OS TCP сокет, `connect()` к destination, аллоцирует `tcp_proxy_server_conn`, регистрирует diag_timer и коллбэки tcp_conn | -| `tcp_proxy_server_handle_data(inst,conn,entry,sid)` | Копирует данные клиента в write_queue OS сокета через pool-аллокацию | -| `tcp_proxy_server_handle_close(inst, sid)` | Паузит чтение, сливает read_queue, отправляет shutdown в OS сокет, освобождает соединение если не connected | -| `tcp_proxy_server_handle_error(inst, sid)` | Делегирует в `handle_close` (обрабатывается идентично CLOSE) | -| `tcp_proxy_server_handle_fin(inst, sid)` | Push-ит FIN в write_queue OS сокета (клиент подтвердил получение FIN от destination) | +## Диспетчеризация и очистка -### Управление соединениями +recv_cb принимает владение entry/dgram, CONNECT/DATA handlers также освобождают их. +Peer берётся из метаданных router, а не из физического conn. +Уведомление router без payload (26 байт) удаляет только потоки данного peer. +Это локальное CLOSE_ALL/restart/close-событие канала, не отдельная 9-байтовая команда proxy. -| Функция | Описание | -|---------|----------| -| `tcp_proxy_server_find_conn(ctx, sid)` | Линейный поиск соединения по stream_id в связном списке | -| `tcp_proxy_server_conn_free(rc)` | Полное освобождение: защита от double-free, отмена таймеров, отправка pending close/error если надо, удаление из списка, `tcp_conn_destroy()`, u_free | +conn_free сразу помечает freed, удаляет поток из списка, отменяет waiter/control retry, +DATA retry и диагностический таймер, уничтожает TCP. Память структуры освобождается +через uasync_call_soon, чтобы текущая синхронная доставка могла проверить freed. +Указатель нельзя использовать как живой поток после conn_free. -### Внутренние callback-и (статические) - -| Функция | Описание | -|---------|----------| -| `send_msg(inst, dst, subcmd, sid, data, len, force)` | Формирует ETCP-пакет (svc_id + subcmd + sid + data) и отправляет через `etcp_route_send()` | -| `send_close(rc)` | Отправляет CLOSE клиенту, при неудаче запускает экспоненциальный ретрай через `close_timer` | -| `send_error(rc)` | Аналогично `send_close`, но шлёт ERROR | -| `send_fin(rc)` | Отправляет FIN клиенту (без ретрая) | -| `on_fin_cb(tc, arg)` | OS сокет получил FIN → машина состояний: если локальный FIN тоже → close; если write pending → ждать flush; если read pending → отложить FIN | -| `on_flushed_cb(tc, arg)` | Write очередь OS сокета опустела → отправить FIN клиенту или закрыть если оба FIN получены | -| `on_error_cb(tc, err, arg)` | Ошибка OS сокета → отправить ERROR клиенту, освободить соединение | -| `on_closed_cb(tc, arg)` | OS сокет полностью закрыт → освободить соединение | -| `read_queue_drain_cb(q, arg)` | Дрейн read_queue OS сокета → отправка данных клиенту через ETCP; backpressure: буферизация в tx_buf + pause_waiter + retry_timer | -| `pause_resume_cb(q, arg)` | ETCP канал освободился → возобновить дрейн read_queue | -| `retry_timer_cb(arg)` | Fallback-таймер 500ms: повтор tx_buf с force=1, при успехе возобновляет read_queue | -| `close_retry_cb(arg)` | Экспоненциальный backoff для повторной отправки CLOSE/ERROR (50→5000 tb) | -| `diag_timer_cb(arg)` | 1-секундный таймер: логирует TCP буферы ОС, размеры очередей, free-counts pool-ов | - -### Константы - -| Макрос | Значение | Описание | -|--------|----------|----------| -| `TCP_PROXY_HDR_SIZE` | 6 | svc_id(1) + subcmd(1) + stream_id(4) | -| `TCP_PROXY_CONNECT_HDR_SIZE` | 12 | HDR_SIZE + dest_ip(4) + dest_port(2) | -| `TCP_PROXY_SUBCMD_CONNECT` | 0x01 | Запрос на создание TCP соединения к destination | -| `TCP_PROXY_SUBCMD_DATA` | 0x03 | Данные для отправки в destination | -| `TCP_PROXY_SUBCMD_CLOSE` | 0x04 | Клиент закрывает прокси-соединение | -| `TCP_PROXY_SUBCMD_ERROR` | 0x05 | Ошибка на стороне клиента | -| `TCP_PROXY_SUBCMD_FIN` | 0x06 | Клиент подтвердил FIN от destination | - -### Зависимости - -| Модуль | Использование | -|--------|--------------| -| `tcp_io.h` | `tcp_conn` — абстракция OS TCP сокета с read/write очередями и memory pool-ами | -| `ll_queue.h` | Очереди read_queue/write_queue OS сокета, pause_waiter для backpressure | -| `etcp_router.h` | `etcp_router_bind()`, `etcp_route_send()`, `etcp_router_on_send_ready()`, `etcp_router_cancel_send_ready()` | -| `u_async.h` | Таймеры (`uasync_set_timeout`, `uasync_cancel_timeout`) | -| `udp_proxy.h` | Авто-инициализация при старте сервера | -| `icmp_proxy.h` | Авто-инициализация при старте сервера | -| `config_parser.h` | Флаг `tcp_proxy_server_enabled` | -| `mem.h` | `u_malloc`/`u_free`/`u_calloc` для tx_buf и структур | +destroy выключает сервер, снимает binding и уничтожает общие UDP/ICMP-контексты. +Клиент того же instance должен быть остановлен раньше; ua должен обработать отложенные free. +Все операции выполняются в uasync. Каждую секунду диагностический таймер пишет +размеры TCP-очередей, наличие write_buf, кеши пулов и OS socket buffers на уровне proxy=trace.