Files
utun2/doc/proxy_protocol.md
T

15 KiB
Raw Blame History

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. ICMP прошёл через test_loopback: raw socket недоступен без соответствующих прав. В успешном SOCKS/HTTP-тесте остались EPIPE/ENOTCONN при teardown, а тестовые конфиги логируют неподдержанный enabled в секции сервера; наблюдения записаны в tasks.md. Полный check.sh, реальные TUN/load/burst и Windows/FreeBSD в этом аудите не запускались. Создание proxy из конфигурации в utun_instance.c исключено под _WIN32. IPv4-фрагменты на входе proxy TUN отклоняются; сборки фрагментов нет. Описания модулей: src/proxy/*_doc.md; все семь заголовков src/proxy/*.h сверены с реализацией.