From 5e80a9cc6c41dab4b504650c0ae936b710bd474f Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 24 Oct 2026 12:38:27 +0200 Subject: [PATCH] docs(proxy): consolidate current protocol and audit coverage --- doc/proxy_protocol.md | 208 ++++++++++++++---- src/proxy/PROTOCOL.md | 486 ++---------------------------------------- 2 files changed, 175 insertions(+), 519 deletions(-) diff --git a/doc/proxy_protocol.md b/doc/proxy_protocol.md index 7332da77..ae9cb74c 100644 --- a/doc/proxy_protocol.md +++ b/doc/proxy_protocol.md @@ -1,61 +1,177 @@ -# Proxy: потоки TCP и датаграммы +# Proxy: TCP, UDP и ICMP -Клиентские входы — TUN (lwIP), SOCKS5 CONNECT и HTTP proxy/CONNECT. Exit открывает -обычный TCP-сокет к назначению. TCP-поток на exit определяется парой `(peer_node_id, -stream_id)`. Клиент принимает ответы и уведомления restart только от настроенного -exit в UTUN-группе. UDP/ICMP-контексты принадлежат конкретному `UTUN_INSTANCE`. +Клиент принимает IPv4 из отдельного TUN через lwIP либо TCP от локальных SOCKS5/HTTP +listeners. Exit открывает обычный IPv4 TCP/UDP или raw ICMP socket. Вся передача proxy +идёт через `etcp_route_send` в `TOPO_GROUP_UTUN`, mode=0. Протокол не имеет слоя +совместимости с прежними форматами. -## TCP +## Сообщения для router и приёмные метаданные -Общие определения находятся в `src/proxy/proxy_protocol.h`. Передаваемый заголовок: -`svc_id:1, command:1, stream_id:4`; поля маршрута добавляет ETCP-router. +Это форматы **сервисного API**, а не полные ETCP wire packets. На отправке первый байт — +svc_id, остальное — payload router. Router callback получает такой заголовок: -- CONNECT (1): IPv4:4 и порт:2 в сетевом порядке. -- CONNECTED (2): exit подтвердил успешный TCP connect. Только после этого клиент - сообщает SOCKS success / HTTP 200 и начинает передавать накопленные данные. -- DATA (3): от 1 до 4096 байт. -- CLOSE (4), ERROR (5): прекращение потока. -- FIN (6): конец одного направления; все предшествующие DATA должны быть переданы. -- WINDOW (7): uint32_t, число дополнительно разрешённых байт. +| Смещение | Размер | Поле | +|---|---|---| +| 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 | -У каждого направления начальное окно 65536 байт. DATA уменьшает окно отправителя -и получателя. Получатель возвращает кредит после записи в локальный TCP-сокет -либо принятия данных ограниченным send-buffer lwIP. Очередь ETCP сама по себе -не является подтверждением потребления данных конечным TCP. +Смещения определены `ROUTER_SVC_*_OFF` в `src/routing_layer/etcp_router.h`. +Заголовок callback имеет размер 26 байт. Сервис освобождает полученные entry/dgram +при любом исходе. На отправке router принимает владение entry/dgram даже при ошибке; +0 означает принятие сообщения router, а не доставку конечному TCP/UDP. -FIN ждёт освобождения исходящей очереди, включая данные, остановленные окном -или backpressure роутера. Закрытие одного направления не прекращает другое. -Окончательное освобождение происходит после обоих FIN и передачи оставшихся данных. -Непереданные управляющие сообщения повторяются таймером. Ошибки и переходы -состояния диагностируются категорией `proxy`; состояние сокетов — `socket`. +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. -Парсеры сохраняют остаток после greeting/request/HTTP headers. HTTP CONNECT ждёт -полного блока заголовков. Во время DNS/connect чтение ограничено очередью tcp_io; -накопленные HTTP headers/body передаются частями размером DATA. +## 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. + +## 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 +после освобождения владельца нет. + +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 -REQUEST и REPLY различаются явно. Ответ допускается только от настроенного exit. -UDP-ответ восстанавливается с нулевой UDP checksum (допустимо для IPv4), IPv4 checksum -пересчитывается. ICMP checksum учитывает нечётный последний байт без чтения за буфером. +Контексты принадлежат конкретному 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 нет. -Exit заменяет ICMP id/sequence уникальной среди ожидающих запросов парой и сопоставляет -ответ также с IP назначения. Исходные id/sequence восстанавливаются для клиента. -Это исключает смешивание ping-запросов разных клиентов с одинаковыми исходными ID. +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). -## Проверки и ограничения +ICMP exit использует один raw socket; заменяет id/seq token и сопоставляет +wire_id/wire_seq/IP цели с pending. Исходные id/seq восстанавливаются для клиента. +Порог ожидания >5 с проверяется периодически. Raw reply проходит проверку ICMP checksum. +Нечётный хвост checksum обрабатывается без чтения за буфером. +test_loopback даёт виртуальный ответ только при отсутствии raw socket. +Успешный ICMP init не гарантирует raw socket: его ошибка оставляет контекст активным. -`test_proxy_packets` проверяет IPv4/UDP/ICMP и payload длиной 0..1501. -`test_tcp_io_flush` проверяет уведомление после последнего синхронного send. -`test_proxy_regressions` использует реальные локальные сокеты и управляемую доставку -ETCP: совместные/раздельные handshake, ранние данные, отложенный CONNECTED, HTTP 502, -большой POST, одинаковые stream ID разных пиров, FIN при закрытом окне, RST и IPv4 options. +Send API принимает payload 0..65507, TUN delivery — 0..65506 из-за однобайтового +префикса в uint16_t длине entry. UDP send требует локальный UDP-контекст, ICMP send +его не требует. Доставка ответа требует клиентский TUN; своего окна/повтора датаграмм нет. -На Linux полный `./check.sh`: 109 passed, 0 failed, 1 skipped; дополнительно прошли -настоящий TUN, full-duplex burst и нагрузка 8 соединений / 128 МиБ. -Windows/FreeBSD в этом прогоне не проверялись. +## Жизненный цикл, диагностика и проверки -SOCKS IPv6 явно отклоняется кодом address type not supported. IPv4-фрагменты -на входе proxy TUN отклоняются с диагностикой; сборка фрагментов не реализована. -Формат TCP дополнен CONNECTED/WINDOW, UDP-команды разделены: клиент и exit необходимо -обновлять вместе; совместимость со старым proxy-протоколом не предусмотрена. +Все операции и callbacks — в uasync владельца. Proxy init запрашивает binding +после router init; client/server имеют разные service IDs. Сервер создаёт UDP/ICMP, +клиент делает это при наличии TUN. Сначала уничтожается клиент, затем сервер: +клиент сохраняет общие UDP/ICMP пока сервер включён, server destroy уничтожает их. +После удаления exit-потоков ua должен обработать отложенные free. +Ошибки listener/bind/подмодулей не всегда пробрасываются init/create — точные контракты в .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_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 загрузки | +| test_udp_proxy / test_icmp_proxy | Два instance через ETCP; ICMP использует raw socket либо тестовый fallback | +| test_tcp_io_flush | Callback после последнего синхронного send | + +Аудит 2026-10-24: перечисленные девять тестовых программ пересобраны и прошли на Linux. +Полный check.sh, реальные TUN/load/burst и Windows/FreeBSD в этом аудите не запускались. +Создание proxy из конфигурации в utun_instance.c исключено под _WIN32. +IPv4-фрагменты на входе proxy TUN отклоняются; сборки фрагментов нет. +Описания модулей: `src/proxy/*_doc.md`; все семь заголовков `src/proxy/*.h` сверены с реализацией. diff --git a/src/proxy/PROTOCOL.md b/src/proxy/PROTOCOL.md index d1958458..4daa6c80 100644 --- a/src/proxy/PROTOCOL.md +++ b/src/proxy/PROTOCOL.md @@ -1,478 +1,18 @@ -## TCP Proxy Protocol — архитектура (v2, без CONNECTED) +# Протокол proxy -### 1. Протокольный формат (общий для client и server) +Актуальное описание TCP/UDP/ICMP, router-метаданных, окон, half-close и проверок: +[doc/proxy_protocol.md](../../doc/proxy_protocol.md). -``` -┌──────┬──────┬───────────────────────┬──────────────────────┐ -│svc_id│subcmd│ stream_id (4) │ data ... │ -│ 1B │ 1B │ │ │ -└──────┴──────┴───────────────────────┴──────────────────────┘ - HDR = 6 байт +TCP ожидает реальный CONNECTED и использует WINDOW; FIN завершает одно направление. +Прежние схемы без CONNECTED, с общим TCP service ID и 9-байтовым CLOSE_ALL не соответствуют +реализации. Общие определения и контракт proxy_flow находятся в [proxy_protocol.h](proxy_protocol.h). -subcmd: - 0x01 CONNECT client→server : dest_ip(4)+dest_port(2) - 0x03 DATA ↔ bidirectional: payload - 0x04 CLOSE ↔ bidirectional: (нет данных) - 0x05 ERROR ↔ bidirectional: (нет данных) -``` +Описание отдельных модулей: ---- +- [TCP client](tcp_proxy_client_doc.md) — TUN/lwIP, маршрутизация к exit, приём ответов. +- [TCP server](tcp_proxy_server_doc.md) — OS connect, окна, очереди, FIN/CLOSE/ERROR. +- [SOCKS и HTTP](socks_proxy_doc.md) — listeners, DNS, HTTP framing/keep-alive/туннели. +- [UDP](udp_proxy_doc.md) — per-instance flows и восстановление ответа в TUN. +- [ICMP](icmp_proxy_doc.md) — raw socket, сопоставление echo, тестовый fallback. -### 2. Диаграмма состояний `tcp_proxy_client_conn` - -``` - ┌──────────────┐ - │ ALLOC + │ accept_cb: u_calloc, stream_id++ - │ CONNECT │ send CONNECT → exit - │ отправлен │ - └──────┬───────┘ - │ - ┌───────────┴───────────┐ - │ send успешен? │ - └───────────┬───────────┘ - Y │ N - ┌────────┘ └──────┐ - ▼ ▼ - ┌────────────┐ ┌──────────────┐ - │ ACTIVE │ │ FREE │ - │ traffic │ └──────────────┘ - └────┬──┬────┘ - │ │ - │ └─────────────────────────────────┐ - │ │ - ▼ lwIP FIN (p=NULL) ▼ ERROR от lwIP/exit - ┌──────────────┐ ┌──────────────┐ - │ TUN_CLOSED │ │ ERROR │ - │ tun_closed=1 │ │ error=1 │ - │ ↓ send_close │ │ send ERROR→ │ - │ после drain │ └──────┬───────┘ - │ tx_buf+lwIP │ │ - └──────┬───────┘ poll_cb: tcp_abort+free - │ - │ recv CLOSE от exit - ▼ - ┌───────────────────┐ - │ TUN+REM CLOSED │ - │ tun=1 rem=1 │ - │ ждём flush буферов│ - └────────┬──────────┘ - │ poll_cb: pending=0, unsent=NULL, unacked=NULL - │ (или sndbuf==0) - ▼ - ┌──────────────┐ - │ CLEANUP │ - │ free(pc) │ - └──────────────┘ - - -ДОП. СОСТОЯНИЯ: -┌─────────────────┐ send_data в ETCP вернул -1 (send_q полон) -│ TX_BACKPRESSURE│ не tcp_recved() → окно lwIP=0 → recv_cb остановлен -│ tx_buf != NULL │ etcp_router_on_send_ready() → ждём освобождения -│ tx_waiter pend │ waiter_cb: retry send_data → tcp_recved → окно открыто -└─────────────────┘ - -┌─────────────────┐ send_close/send_error вернул -1 -│ CLOSE_PENDING │ → retry в poll_cb -│ close_pending=1│ -└─────────────────┘ -``` - ---- - -### 3. Диаграмма состояний `tcp_proxy_server_conn` - -``` - ┌───────────────────┐ - │ CREATING │ recv CONNECT - │ socket() создан │ → неблокирующий connect() - │ sock = неблок. │ - └────────┬──────────┘ - │ - ┌────────────┴────────────┐ - │ connect() результат? │ - └────┬──────────┬──────────┘ - EINPROGRESS│ │ошибка сразу - ▼ ▼ - ┌──────────┐ ┌──────────────┐ - │CONNECTING│ │ ERROR │ send ERROR→client - │ждём write│ │ FREE │ - └────┬─────┘ └──────────────┘ - │ write_cb: SO_ERROR==0 - ▼ - ┌─────────────┐ - │ CONNECTED │ connected=1 - │ flush pend │ → flush pending_buf в сокет - └──────┬──────┘ - │ - ┌──────────────┼──────────────┐ - │ │ │ - client→DATA │ │ ERROR (sock/pipe) - (sock_send) │ ▼ - или pending_buf │ ┌──────────────┐ - │ │ │ ERROR │ send ERROR→client - │ │ │ FREE │ - │ sock EOF (n=0) │ │ - │ │ └──────────────┘ - │ ▼ - │ ┌──────────────────┐ - │ │ SOCK_CLOSED │ sock_closed=1 - │ │ out_buf/pause? │ если очереди пусты → send_close - │ │ ждём drain │ иначе ждём retry_cb/waiter_cb - │ └────────┬─────────┘ - │ │ очереди опустошены → send_close - │ │ если cli_closed → FREE - │ │ - client→CLOSE │ - │ │ - ▼ │ - ┌──────────────┐ │ - │ CLI_CLOSED │ │ cli_closed=1 - │ out_buf pend? │◄────────┘ если out_buf пуст → shutdown(SHUT_WR) - │ ждём flush │ иначе ждём sock_retry_cb → shutdown - └──────┬───────┘ - │ - └──────────────┐ - ▼ - ┌──────────────────┐ - │ cli && sock │ оба закрыты + очереди пусты - │ очереди пусты │ - └────────┬─────────┘ - ▼ - ┌──────────────┐ - │ FREE │ conn_free(rc) - └──────────────┘ - - -ДОП. СОСТОЯНИЯ: -┌─────────────────┐ send_msg в ETCP вернул -1 -│ PAUSED │ буферизуем в pause_buf, убираем read_id (пауза чтения) -│ pause_buf>0 │ etcp_router_on_send_ready() → ждём освобождения -│ pause_waiter │ waiter_cb: retry send_msg → resume read_id -└─────────────────┘ - -┌─────────────────┐ CLOSE/ERROR не доставлен через ETCP -│ CLOSE_PENDING │ close_timer с backoff 50..5000 tb → ретрай -│ close_pending=1│ -└─────────────────┘ -``` - ---- - -### 4. DATA FLOW - -``` -══════════════════════════════════════════════════════════════════════ - APP → lwIP → recv_cb → ETCP → handle → socket → DEST - APP ← lwIP ← tcp_write ← ETCP ← read_cb ← socket ← DEST -══════════════════════════════════════════════════════════════════════ - - CLIENT (узел A) SERVER (узел B) - - APP (через TUN) DEST (реальный) - │ │ - ▼ TCP SYN/ACK (lwIP) ▼ TCP SYN/ACK (OS) - ┌──────────┐ ┌──────────┐ - │ lwIP │ │ socket │ - │ TCP │ │ (неблок) │ - └────┬─────┘ └────┬─────┘ - │ recv_cb │ read_cb - │ │ - ┌────▼────────────┐ ┌────▼───────────────┐ - │ tcp_proxy │ ETCP │ tcp_proxy │ - │ _client │◄═══════════════════════►│ _server │ - │ │ │ │ - │ ┌────────────┐ │ │ ┌──────────────┐ │ - │ │ recv_cb │ │──CONNECT+DATA──► │ │handle_connect│ │ - │ │(lwIP→ETCP) │ │ │ │handle_data │ │ - │ │ │ │ backpressure: │ └──────┬───────┘ │ - │ │ send fail: │ │ waiter на send_q │ │ sock_send │ - │ │ tx_buf+ │ │ │ ┌──────▼───────┐ │ - │ │ waiter │ │ │ │ out_buf │ │ - │ │ окно lwIP=0│ │ │ │ pending_buf │ │ - │ └────────────┘ │ │ └─────────────┘ │ - │ │ │ │ send() │ - │ ┌────────────┐ │ │ ┌──────▼───────┐ │ - │ │handle_data │ │◄────DATA──────── │ │ read_cb │ │ - │ │handle_close│ │ │ │(sock→ETCP) │ │ - │ └─────┬──────┘ │ │ │ │ │ - │ │ │ │ │ send fail: │ │ - │ ┌─────▼──────┐ │ │ │ pause_buf+ │ │ - │ │ to_lwip │ │ │ │ waiter+ │ │ - │ │ (очередь) │ │ │ │ пауза read_id│ │ - │ └─────┬──────┘ │ │ └─────────────┘ │ - │ │tcp_write│ │ │ - │ ▼ │ └───────────────────┘ - │ lwIP→TUN→APP │ - └─────────────────┘ -``` - ---- - -### 5. BACKPRESSURE (lwIP→ETCP и socket→ETCP) - -``` -══════════════════════════════════════════════════════════════════════ - - Используется встроенный механизм ll_queue threshold waiter (на router send_q): - etcp_router_on_send_ready(inst, group_id, node_id, svc_id, &waiter, cb, arg) - → внутри: queue_waiter_wait(rconn->send_q, &waiter) - etcp_router_cancel_send_ready(inst, group_id, node_id, svc_id, &waiter) - - CLIENT (lwIP → ETCP): - recv_cb(buf, len): - ret = send_data(buf, len) - if ret == 0: tcp_recved(len) - if ret < 0: - tx_buf = copy(buf), tx_len = len - НЕ tcp_recved() → окно lwIP = 0, recv_cb больше не вызывается - etcp_router_on_send_ready(inst, group_id, via_node_id, ETCP_RT_ID_TCP_PROXY_SERVER, &tx_waiter, tx_waiter_cb, pc) - - tx_waiter callback: - ret = send_data(tx_buf, tx_len) - if ret == 0: - tcp_recved(tx_len) → окно открыто, lwIP возобновляет recv_cb - free(tx_buf) - if tun_closed → send_close() - // если ret < 0 → waiter остаётся, router вызовет снова - - SERVER (socket → ETCP): - read_cb(buf, n): - ret = send_msg(DATA, buf, n) - if ret == 0: ok - if ret < 0: - pause_buf = copy(buf), pause_len = n - uasync_remove_socket_t(sock) → пауза чтения с сокета - etcp_router_on_send_ready(inst, group_id, peer_node_id, ETCP_RT_ID_TCP_PROXY_CLIENT, &pause_waiter, pause_waiter_cb, rc) - - pause_waiter callback: - ret = send_msg(DATA, pause_buf, pause_len) - if ret == 0: - free(pause_buf) - uasync_add_socket_t(sock) → возобновляем чтение - if sock_closed → send_close() - // если ret < 0 → waiter остаётся -``` - ---- - -### 6. Последовательность: HANDSHAKE (упрощённый, без CONNECTED) - -``` -CLIENT (lwIP) CLIENT PROXY ETCP SERVER PROXY SOCKET -───────────────────────────────────────────────────────────────────────────────────────── - - tcp_proxy_client_accept_cb() CONNECT - ├─ u_calloc(conn) ────────────────────────────→ tcp_proxy_server_handle_connect() - ├─ stream_id++ ├─ socket(AF_INET,SOCK_STREAM) - ├─ send_connect() ├─ socket_set_nonblocking() - │ [dest_ip(4)+dest_port(2)] ├─ uasync_add_socket_t(read/write/error) - ├─ conn→conns list ├─ connect(неблок.) → EINPROGRESS - │ └─ rc→conns list - ▼ - ACTIVE — данные идут СРАЗУ (без ожидания CONNECTED) - │ - │ DATA ...connect завершён... - ├──────────────────────────────────────────────────→ tcp_proxy_server_sock_write_cb() - │ ├─ connected=1 - │ DATA └─ flush pending_buf в сокет - ├──────────────────────────────────────────────────→ - │ - │ DATA ←─ tcp_proxy_server_sock_read_cb() - │◄────────────────────────────────────────────── (socket recv → ETCP) - │ - └──→ трафик идёт в обе стороны - - При ошибке connect: - ...connect fail... → tcp_proxy_server_send_error() → client получает ERROR, error=1, cleanup -``` - ---- - -### 7. Последовательность: CLOSE (drain очередей → close в конце) - -``` -CLIENT (lwIP) CLIENT PROXY ETCP SERVER PROXY SOCKET -───────────────────────────────────────────────────────────────────────────────────────── - -══ Вариант А: lwIP закрывает первым ══ - - tcp_proxy_client_recv_cb(p=NULL) - ├─ tun_closed=1 - │ если tx_buf пуст → send_close CLOSE - │ если tx_buf pend → waiter досылает ──────────→ tcp_proxy_server_handle_close() - │ потом send_close ├─ cli_closed=1 - └─ │ если out_buf пуст → shutdown(SHUT_WR) - │ иначе ждём sock_retry_cb - │ - ...out_buf дослан... - sock_retry_cb: - shutdown(SHUT_WR) - │ - ...сокет дочитывает остаток... - │ - tcp_proxy_server_sock_read_cb(n=0) - ├─ sock_closed=1 - │ если pause_buf пуст → send_close - │ иначе ждём waiter_cb - │ - CLOSE │ - tcp_proxy_client_handle_close() ◄────────────────────────┘ - └─ rem_closed=1 - │ - poll_cb: tun=1, rem=1, to_lwip пуст, unsent=NULL, unacked=NULL - ├─ tcp_close(pcb) - └─ conn_free() - - -══ Вариант Б: серверный сокет закрывается первым ══ - - tcp_proxy_server_sock_read_cb(n=0) - ├─ sock_closed=1 - │ если out_buf+pause_buf пусты → send_close - │ иначе ждём retry/waiter - CLOSE │ - tcp_proxy_client_handle_close() ◄────────────────────────┘ - └─ rem_closed=1 - │ - ▼ - ...приложение закрывает TCP... - │ - tcp_proxy_client_recv_cb(p=NULL) - ├─ tun_closed=1 - │ если tx_buf пуст → send_close - └─ - - poll_cb: оба закрыты, буферы пусты → cleanup - - -══ Вариант В: Ошибка / аномалия ══ - - ERROR - ─────────────────────── (или ←) ────────── - error=1 - │ - ▼ - poll_cb: tcp_abort(pcb) → немедленная очистка - или: rc→error=1 → conn_free - - -══ Вариант Г: conn не найден / closed / error ══ - - Любой пришедший пакет (DATA/CLOSE/ERROR) для conn в закрытом состоянии: - → tcp_proxy_*_send_msg(ERROR, stream_id) - → удалённая сторона получает ERROR и немедленно закрывается -``` - ---- - -### 8. Диспетчеризация в `tcp_proxy_client_etcp_recv_cb()` - -``` -┌────────────────────────────────────────────────────────────────────────┐ -│ tcp_proxy_client_etcp_recv_cb() │ -│ (единая точка входа для обоих направлений) │ -├────────────────────────────────────────────────────────────────────────┤ -│ │ -│ subcmd == CONNECT? │ -│ └── tcp_proxy_server_handle_connect() ←── всегда сервер │ -│ │ -│ stream_id в server.conns? (если server.enabled) │ -│ ├── DATA → tcp_proxy_server_handle_data() │ -│ ├── CLOSE → tcp_proxy_server_handle_close() │ -│ └── ERROR → rc->error=1; conn_free() │ -│ │ -│ stream_id в client.conns? (если client proxy != NULL) │ -│ ├── DATA → tcp_proxy_client_handle_data() │ -│ ├── CLOSE → tcp_proxy_client_handle_close() │ -│ └── ERROR → tcp_proxy_client_handle_error() │ -│ │ -│ Приоритет: │ -│ 1. CONNECT всегда интерпретируется сервером │ -│ 2. Если server.enabled И stream_id найден в server.conns → server │ -│ 3. Иначе если client proxy != NULL → client │ -│ 4. Иначе drop │ -└────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -### 9. INIT - -``` -CLIENT SERVER -───────────────────────────────────────────────────────── - -utun_instance.c - │ - ├─ tcp_proxy_client_create() tcp_proxy_server_init() - │ ├─ tun_init_nat() (TUN iface) ├─ memset(ctx,0) - │ ├─ lwip_tcp_init() (TCP стек) ├─ ctx->enabled = config ... - │ ├─ etcp_router_bind( ├─ etcp_router_bind( - │ │ ETCP_ID_TCP_PROXY, │ ETCP_ID_TCP_PROXY, - │ │ tcp_proxy_client_etcp_recv_cb) │ tcp_proxy_client_etcp_recv_cb) - │ ├─ udp_proxy_init() ├─ udp_proxy_init() - │ └─ icmp_proxy_init() └─ icmp_proxy_init() (если client не активен) - │ - └─ ОБА слушают ETCP_ID_TCP_PROXY через один обработчик -``` - ---- - -### 10. Поля структур - -``` -tcp_proxy_client_conn: - stream_id — уникальный ID потока - tun_closed — lwIP/FIN (сторона TUN) - rem_closed — CLOSE от exit - error — немедленная очистка - close_sent — CLOSE/ERROR отправлен - close_pending — отправка не удалась, ретрай - to_lwip — очередь DATA от exit → lwIP (feed_from_transport) - pcb — lwIP tcp_pcb - tx_buf — буфер при backpressure lwIP→ETCP (send_data fail) - tx_len — длина tx_buf - tx_waiter — waiter на router send_q (etcp_router_on_send_ready) - -tcp_proxy_server_conn: - stream_id — уникальный ID потока - cli_closed — клиент прислал CLOSE - sock_closed — сокет получил EOF - error — ошибка - connected — сокет подключился (connect завершён) - close_pending — CLOSE/ERROR не доставлен, ретрай - pending_buf — буфер данных от клиента до завершения connect - out_buf — буфер отправки в сокет (неблокирующий send, EAGAIN retry) - out_off/out_len — offset/размер out_buf - out_timer — таймер повтора send в сокет - out_backoff — backoff для retry send в сокет - pause_buf — буфер при backpressure socket→ETCP (send_msg fail) - pause_len — длина pause_buf - pause_waiter — waiter на router send_q (etcp_router_on_send_ready) - close_timer — таймер повтора CLOSE/ERROR - close_backoff — backoff для retry CLOSE/ERROR - sock — OS сокет к адресату - read_id — handle uasync для событий сокета -``` - ---- - -### 11. etcp_router waiter API - -``` -// etcp_router.h -void etcp_router_on_send_ready(struct UTUN_INSTANCE* inst, uint64_t group_id, - uint64_t node_id, uint8_t svc_id, struct queue_waiter_handle* h, - queue_threshold_callback_fn cb, void* arg); -void etcp_router_cancel_send_ready(struct UTUN_INSTANCE* inst, uint64_t group_id, - uint64_t node_id, uint8_t svc_id, struct queue_waiter_handle* h); - -// Внутри: -// rconn = router_conn_find(inst, group_id, node_id, svc_id) -// queue_waiter_wait(rconn->send_q, h, cb, arg) -// queue_waiter_cancel(rconn->send_q, h) -// -// send_q — очередь router conn при полном inflight -// (threshold = ROUTER_MAX_SEND_Q_PACKETS - 1 в router_conn_reset) -``` +Контракты функций, буферов и полей HTTP codec — в [http_proxy.h](http_proxy.h).