# TCP proxy client `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_proxy_client_create` вызывается в uasync с живым `ua`. Для TUN нужны **оба** непустых `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. `inst/ua/mappings` заимствуются до destroy; результат принадлежит вызывающему коду. Создание само не заполняет `inst->tcp_proxy_client`: это делает владелец, например `utun_instance`. Для автономного TUN smoke test допускается inst=NULL, но для обмена через router нужен instance. Mappings содержат порты в host byte order, адрес назначения — IPv4-строку. Все заданные SOCKS/HTTP listeners, TUN mappings, TUN/lwIP/пулы и TCP router binding обязательны. Ошибка любого шага возвращает NULL и освобождает уже созданные ресурсы. Повторное создание клиента или занятой binding отклоняются. UDP/ICMP создаёт владелец instance после успешного создания TCP-модулей, если есть TUN либо включён exit. Клиент привязывается к `ETCP_RT_ID_TCP_PROXY_CLIENT` (0x08), сервер — к `ETCP_RT_ID_TCP_PROXY_SERVER` (0x04). Генератор stream_id общий для TUN/SOCKS/HTTP, изначально равен 1, перед выдачей инкрементируется. На exit ключ потока — пара peer/sid. ## 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. При 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. 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 возвращается в начало очереди. ## Exit → TUN Router callback проверяет peer==via_node_id и group==TOPO_GROUP_UTUN, включая пустые уведомления router. Сначала предлагает stream SOCKS/HTTP спискам, затем ищет lwIP-поток. 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`. ## Завершение Локальный 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)`; при неудаче попытка повторяется. Когда обе половины завершены, PCB отсоединяется от callbacks: lwIP может самостоятельно освободить его по TIME_WAIT без err callback. Поток удаляется после отправленного FIN, полученного FIN, снятого fin_deferred и пустых tx_queue/to_lwip. Штатное завершение не требует дополнительного обмена CLOSE. Входящий CLOSE прекращает поток через `tcp_close` и очистку, ERROR вызывает abort. Ошибка lwIP обнуляет PCB, который уже освобождает стек; остальная очистка отменяет control retry, DATA timer, router waiter и освобождает обе очереди. CLOSE/ERROR при неудаче отправки могут повторяться через close_pending в poll/free пока поток жив; отдельной сохраняемой очереди этих команд нет. Пустое уведомление router (`ROUTER_SVC_HDR_SIZE=26`, без TCP command/sid) удаляет все lwIP/SOCKS/HTTP потоки клиента к настроенному exit. Это не 9-байтовый wire CLOSE_ALL. `destroy` снимает только собственный TCP binding, закрывает listeners/клиентов, освобождает lwIP/TUN/пул. Общие UDP/ICMP принадлежат instance и сохраняются. Владелец затем сам обнуляет `inst->tcp_proxy_client`. Все callbacks и очистка выполняются в uasync; уничтожать клиента следует вне его активного callback.