# Proxy: TCP, UDP и ICMP Клиент принимает IPv4 из отдельного TUN через lwIP либо TCP от локальных SOCKS5/HTTP listeners. Exit открывает обычный IPv4 TCP/UDP или raw ICMP socket. Вся передача proxy идёт через `etcp_route_send` в `TOPO_GROUP_UTUN`, mode=0. Протокол не имеет слоя совместимости с прежними форматами. ## Сообщения для router и приёмные метаданные Это форматы **сервисного API**, а не полные ETCP wire packets. На отправке первый байт — svc_id, остальное — payload router. Router callback получает такой заголовок: | Смещение | Размер | Поле | |---|---|---| | 0 | 1 | svc_id | | 1 | 8 | src_node_id | | 9 | 8 | dst_node_id | | 17 | 1 | rx_flags | | 18 | 8 | group_id | | 26 | N | service payload | Смещения определены `ROUTER_SVC_*_OFF` в `src/routing_layer/etcp_router.h`. Заголовок callback имеет размер 26 байт. Сервис освобождает полученные entry/dgram при любом исходе. На отправке router принимает владение entry/dgram даже при ошибке; 0 означает принятие сообщения router, а не доставку конечному TCP/UDP. TCP-клиент проверяет src_node_id==via_node_id и group_id==TOPO_GROUP_UTUN, включая пустые уведомления. UDP/ICMP допускают REQUEST при включённом exit, REPLY — только при наличии клиента и совпадении src_node_id с его via_node_id; отдельной проверки group в их callback нет. IP, TCP/UDP-порты и ICMP id/seq в proxy API передаются в network byte order. TCP stream_id и WINDOW credit копируются как host-order uint32_t: формат не нормализован между архитектурами с разным byte order. ## TCP и управление окном Определения — `src/proxy/proxy_protocol.h`. На отправке: `svc:1, cmd:1, stream_id:4, data` (заголовок 6 байт). На приёме: router header(26), cmd(1), stream_id(4), data (заголовок 31 байт). Клиент отправляет в `ETCP_RT_ID_TCP_PROXY_SERVER=0x04`, exit отвечает в `ETCP_RT_ID_TCP_PROXY_CLIENT=0x08`. На exit поток определяется парой peer_node_id/stream_id; повторный CONNECT этой пары отклоняется с ERROR. | Команда | Код | Содержимое после stream_id | |---|---|---| | CONNECT | 1 | IPv4:4, порт:2 | | CONNECTED | 2 | Пусто; exit завершил реальный TCP connect | | DATA | 3 | 1..4096 байт | | CLOSE | 4 | Пусто; прекращение потока с дренажом принятой записи на exit | | ERROR | 5 | Пусто; отмена, на exit немедленно уничтожает TCP и очереди | | FIN | 6 | Пусто; конец одного направления после всех его DATA | | WINDOW | 7 | uint32_t дополнительного кредита, ровно 4 байта | Каждое направление начинает с окна 65536 байт. proxy_flow init оставляет ready=0. DATA ждёт CONNECTED; SOCKS success и HTTP CONNECT 200 также отправляются только после него. Локальный TUN TCP handshake выполняет lwIP и сам по себе connect назначения не подтверждает. При отправке DATA списывается tx_credit, при приёме — rx_credit. WINDOW допускает только ненулевой кредит без увеличения tx_credit сверх 65536. Кредит возвращается после потребления локальным TCP: на OS-сокетах после опустошения write_queue **и write_buf**, в lwIP после принятия байтов ограниченным send-buffer через tcp_write. Это не подтверждение чтения удалённым приложением. Для HTTP учитывается число исходных upstream байтов независимо от преобразования headers/chunks/trailers; кредит возвращается после доставки соответствующего результата в OS TCP. Backpressure router и окно потока независимы. При ошибке DATA остаётся в очереди/буфере владельца, регистрируется send-ready waiter и safety-net на 500 мс. У TCP-клиента с lwIP timer повторяет DATA с force=1, у exit и SOCKS/HTTP — обычный force=0. Force обходит лимит send_q router, но никогда не окно proxy. При отсутствии CONNECTED/кредита (-2) отправка ждёт ready/WINDOW, а не только router waiter. OS TCP сохраняет ограничение read_high_water и явную остановку чтения при HUP. Если HUP не оставляет работы для чтения/записи, tcp_io временно снимает регистрацию fd; low_water восстанавливает её с новой generation. Сокет и очереди сохраняются, запись обслуживается независимо от чтения. Ошибка повторной регистрации закрывает соединение. ## Half-close и удаление потока Владелец выставляет fin_pending только после отправки всех исходящих DATA. proxy_flow не видит очередей владельца и сам это условие не проверяет. FIN не должен ждать записи противоположного направления: обе половины независимы. Приём FIN ставит FIN-маркер после ранее принятых DATA в OS write_queue; в lwIP FIN откладывается до дренажа to_lwip и успешного tcp_shutdown. Окончательное закрытие сырого TCP наступает после обоих FIN и необходимых локальных очередей/shutdown. Дополнительный обмен CLOSE при штатном half-close не требуется. На exit CLOSE паузит чтение, отбрасывает непрочитанный read_queue и закрывает сокет после принятой write_queue; ERROR уничтожает поток сразу. На клиенте CLOSE/ERROR прекращают поток; HTTP вне туннеля дополнительно проверяет завершённость ответа. Общий retry повторяет pending CONNECTED → WINDOW → FIN через 5000 tb (500 мс), используя force=1. CONNECT/CLOSE/ERROR в эту очередь не входят. Клиентский close_pending может повторяться в poll/free пока поток жив; сервер после ошибки TCP удаляет поток и делает попытку ERROR. Гарантированного повторения CLOSE/ERROR после освобождения владельца нет. Поздний WINDOW отсутствующего потока отбрасывается с debug-диагностикой, без создания потока. Router close/restart уведомляет сервис entry длиной 26 байт без payload. Proxy называет это CLOSE_ALL: exit удаляет потоки указанного peer, клиент — все свои TUN/SOCKS/HTTP потоки к настроенному exit. Это не самостоятельная подкоманда или 9-байтовый пакет на проводе. ## SOCKS5 и HTTP SOCKS реализует CONNECT к IPv4 или DNS-имени, метод аутентификации 0 (no auth). IPv6 отклоняется с REP=8; BIND и UDP ASSOCIATE не реализованы. DNS выполняется асинхронно на ingress, выбирается первая A-запись. Совмещённые и разделённые greeting/request/data сохраняют непрочитанный хвост. Ошибка DNS/connect до успеха даёт SOCKS REP=4 либо HTTP 502. HTTP/1.0–1.1 forward proxy требует absolute http:// URL; upstream получает origin-form, Host из URL и Via. Обычный запрос отправляется как HTTP/1.1 с Connection: close, кроме Upgrade. Frontend HTTP/1.1 может сохранять keep-alive: каждый запрос получает новый stream_id/upstream, pipelining обслуживается последовательно, включая разные цели. Новый stream начинается после доставки полного ответа и отправки CLOSE/ERROR старого. Codec проверяет headers до пересылки, поддерживает Content-Length, chunked с extensions/trailers, промежуточные ответы, HEAD/204/304 и тело до EOF. Hop-by-hop и proxy authentication поля удаляются, framing/Connection строятся заново. HTTP/1.0 получает декодированное chunked тело и закрытие. CONNECT ждёт полного блока headers и реального CONNECTED; согласованный 101 переводит соединение в сырой туннель. Ранний final response прекращает upload и требует закрытия клиента. Ошибка до final headers может дать собственный HTTP error; после них закрывает оборванный ответ. Лимиты: headers/trailers 32 KiB, start line и chunk/trailer line 8 KiB, 512 header fields. CL+TE, разные Content-Length, obs-fold, управляющие байты и неверные порты отклоняются. Поддержан только transfer coding chunked; HTTP IPv6-назначения отклоняются с 502. Headers имеют deadline 30 с, DNS/connect — 15 с, передача/idle/дренаж закрытия — 60 с. Deadline headers не продлевается каждым байтом. Установленные CONNECT/Upgrade-туннели используют TCP lifecycle без HTTP deadline. Детали codec — в `src/proxy/http_proxy.h`. ## UDP и ICMP Контексты принадлежат конкретному UTUN_INSTANCE. REQUEST=1, REPLY=2. На отправке UDP: `svc:1, cmd:1, src_ip:4, src_port:2, dst_ip:4, dst_port:2, payload`. ICMP: `svc:1, cmd:1, dst_ip:4, orig_src_ip:4, echo_id:2, echo_seq:2, payload`. Заголовки отправки — 14 байт, приёма — 39. Отдельного sender_node_id в payload нет. UDP exit создаёт сокет на tuple client_node_id/src_ip/src_port/dst_ip/dst_port, проверяет источник ответа, меняет src/dst местами в REPLY. Удаляет неактивные flows при очередной периодической проверке с порогом >60 с. Клиент восстанавливает IPv4 checksum, UDP checksum=0 (допустимо для IPv4). Flow публикуется только после socket/nonblocking/bind/read registration и создания таймера очистки. Ошибка продления общего таймера закрывает оставшиеся flows. sendto проверяется по точной длине, включая успешный нулевой payload; EINTR повторяется. EAGAIN/ENOBUFS и ошибки отдельного пакета логируются без очереди повторов; сломанный сокет удаляется. Ошибки выделения ответа, router send и доставки в TUN диагностируются. ICMP exit использует один raw socket; заменяет id/seq token и сопоставляет wire_id/wire_seq/IP цели с pending. Исходные id/seq восстанавливаются для клиента. Порог ожидания >5 с проверяется периодически. Raw reply проходит проверку ICMP checksum. Нечётный хвост checksum обрабатывается без чтения за буфером. `icmp_proxy_init` создаёт общий контекст и binding; `icmp_proxy_enable_exit` отдельно открывает raw socket. EPERM/EACCES/отсутствие поддержки raw отключают только ICMP exit с WARN; клиентские REPLY сохраняются. Прочие ошибки запуска прерывают создание instance. Тестовый `test_loopback` включается явно, закрывает raw socket и даёт виртуальные ответы независимо от прав процесса. Production его не включает. Pending raw request создаётся только с рабочим таймером истечения срока ожидания. Send API принимает payload 0..65507, TUN delivery — 0..65506 из-за однобайтового префикса в uint16_t длине entry. UDP send требует локальный UDP-контекст, ICMP send его не требует. Доставка ответа требует клиентский TUN; своего окна/повтора датаграмм нет. ## Жизненный цикл, диагностика и проверки Все операции и callbacks — в uasync владельца. Proxy init запрашивает binding после router init; client/server имеют разные service IDs. Общие UDP/ICMP создаёт и уничтожает UTUN_INSTANCE; TCP-модули освобождают только собственные ресурсы. Контексты нужны для включённого exit либо клиентского TUN; SOCKS/HTTP без TUN их не требуют. Заданные listeners, TUN mappings и bindings обязательны: ошибка откатывает создание. Повторный init активного модуля и попытка занять чужой binding отклоняются. При уничтожении instance TCP-клиент/сервер и общие контексты закрываются до остановки групп и транспорта; ua обрабатывает отложенные free. Точные контракты — в .h. Категория proxy: info — подключения/закрытия/транзакции, debug — окна/паузы/повторы, trace — трафик и секундная диагностика exit TCP-очередей/пулов/OS buffers. Для поиска проблемы также полезны socket и etcp_route. Регрессионный тест включает proxy=debug через UTUN_TEST_DEBUG=1. | Тест | Что проверяет | |---|---| | test_http_proxy | HTTP codec, разбиения headers/chunks, framing, лимиты, source credit, Upgrade | | test_proxy_packets | IPv4/UDP/ICMP, checksum и payload длиной 0..1501 | | test_proxy_regressions | Handshake/CONNECTED, HTTP pipeline/EOF/deadlines, окна/FIN, peer+sid, reentrancy, TUN options/fragment validation, per-instance UDP/ICMP | | test_proxy_lifecycle | 25 сценариев отказов/rollback, fd/память/таймеры, владение bindings, ICMP без raw, HUP re-register и destroy из callback | | test_tcp_proxy_client | Create/destroy с тестовым TUN, без утечек | | test_tcp_proxy_server | TCP echo через tcp_conn; сам по себе не проверяет протокол exit | | test_socks_http_proxy | Реальные SOCKS IPv4/domain, HTTP и HTTPS CONNECT, byte-exact загрузки; штатный TLS shutdown либо UTUN_TEST_TLS_ABRUPT=1 | | test_udp_proxy / test_icmp_proxy | Два instance через ETCP; ICMP: явный test_loopback, UTUN_TEST_ICMP_RAW=1 требует настоящий raw socket | | test_tcp_io | 19 сценариев, включая HUP high/low-water на epoll/poll, ручную паузу и независимую запись | | test_tcp_io_flush | Callback после последнего синхронного send | Проверка 2026-10-24: профильные тесты проходят на Linux. ICMP проверен и через явный test_loopback, и настоящим raw echo до 127.0.0.1 с правами root. Штатный TLS shutdown проходит без EPIPE/ENOTCONN; аварийный сохраняет реальные ошибки записи, все 16 загрузок остаются byte-exact. Тестовые конфиги используют поддержанный connect_timeout. Итоги полного прогона и sanitizer-проверок — в [proxy_fix_plan.txt](proxy_fix_plan.txt). Windows/FreeBSD runtime в этом окружении не проверены. Создание proxy из конфигурации в utun_instance.c исключено под _WIN32. IPv4-фрагменты на входе proxy TUN отклоняются; сборки фрагментов нет. Описания модулей: `src/proxy/*_doc.md`; все семь заголовков `src/proxy/*.h` сверены с реализацией.