15 KiB
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.
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
Контексты принадлежат конкретному 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).
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: его ошибка оставляет контекст активным.
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, клиент делает это при наличии 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 сверены с реализацией.