Files
utun2/doc/proxy_protocol.md
T

201 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` сверены с реализацией.