181 lines
15 KiB
Markdown
181 lines
15 KiB
Markdown
# 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](tasks.md).
|
||
Полный check.sh, реальные TUN/load/burst и Windows/FreeBSD в этом аудите не запускались.
|
||
Создание proxy из конфигурации в utun_instance.c исключено под _WIN32.
|
||
IPv4-фрагменты на входе proxy TUN отклоняются; сборки фрагментов нет.
|
||
Описания модулей: `src/proxy/*_doc.md`; все семь заголовков `src/proxy/*.h` сверены с реализацией.
|