Merge remote-tracking branch 'origin/master'

This commit is contained in:
evgeny committed 2026-10-24 14:33:18 +02:00
commit f92c78a2e0
32 files changed
+1709 -1296

No files matched your search

+198
View File
@@ -0,0 +1,198 @@
Proxy: исправления и проверка архитектуры
Дата: 2026-10-24. Статус: реализовано и проверено на Linux.
Ниже сохранён согласованный проект; фактические изменения и результаты — в конце.
Подтверждённые факты до исправлений
1. udp_proxy.c не проверяет bind/sendto, результат socket_set_nonblocking,
часть выделений/регистраций/таймеров и отправку ответа через router.
Неудачный запуск flow может оставлять ресурс без рабочего таймера очистки.
2. server_init и client_create оба запускают общие UDP/ICMP-контексты.
client_destroy сохраняет их по server.enabled, server_destroy уничтожает всегда.
Простой rollback через destroy одного модуля не выражает владение общими ресурсами.
3. ICMP init публикует контекст и сообщает успех без raw socket. При этом тот же
контекст нужен для клиентских REPLY: отсутствие возможности exit не означает,
что клиентская сторона ICMP не может работать через другой exit.
4. В диагностическом прогоне tcp_io error_cb при HUP отменяет read_waiter,
включает чтение и напрямую вызывает read_cb вопреки read_paused.
Для fd=15 порог чтения 4; журнал фиксирует read_paused=1 и рост rq до 53.
Это нарушение ограничения очереди, а не только избыточная диагностика.
5. EPIPE/ENOTCONN в HTTPS CONNECT воспроизведены. Системная трасса четырёх TLS-потоков:
recv(destination)=0 -> поздняя TLS-запись 24 байта от клиента -> send(destination)=24
-> SO_ERROR=EPIPE либо shutdown(SHUT_WR)=ENOTCONN. Повторный shutdown не обнаружен.
Определение содержимого зашифрованной TLS-записи по трассе невозможно.
Сам по себе этот сценарий не доказывает дефект half-close в proxy.
6. После удаления exit-потока приходят поздние WINDOW: диспетчер пишет unhandled,
хотя это известная команда уже завершённого потока. Создавать поток по WINDOW нельзя.
7. enabled=yes в серверных тестовых конфигах не поддержан парсером. Сейчас сервер
включается при обработке любого параметра секции, включая неизвестный.
Предлагаемая архитектура
Владелец общих UDP/ICMP-контекстов — UTUN_INSTANCE. Их жизненный цикл координируется
в utun_instance.c после router_init и до router_destroy. TCP client/server управляют
только своими listeners, TCP-потоками и bindings. Общих счётчиков ссылок не требуется:
при существующем lifecycle они живут до остановки instance.
Контексты нужны при включённом exit или клиентском TUN; SOCKS/HTTP без TUN их не требуют.
server.enabled обозначает действующий TCP exit. Роль при запуске ICMP exit должна
передаваться явно, а не определяться случайным порядком вызовов init.
При отказе создания клиента rollback освобождает только его ресурсы и собственный binding.
Повторное создание клиента/сервера поверх активного состояния отклоняется.
Результат client_create публикуется в inst->tcp_proxy_client только после успеха.
Порядок остановки: клиент и exit-потоки -> общие UDP/ICMP -> router -> drain deferred free.
При destroy всего instance остановку proxy перенести перед остановкой UTUN-группы,
разборкой topo_groups и ETCP-сокетов/соединений. Сейчас proxy уничтожается в phase M,
после topo_groups и транспорта: для нового rollback нельзя сохранять этот поздний порядок.
Общий instance_init_common уже вызывает instance_destroy при отказе создания;
нужно использовать этот путь, не добавляя параллельную систему частичной очистки.
При create_from_config сохранить существующее правило: отказ возвращает config вызывающему коду.
Ресурсы закрываются в одном uasync-потоке; stop/destroy не вызывается из активного
callback уничтожаемого владельца. Таймеры/waiters отменяются до освобождения памяти.
Инициализация и ICMP
Разделить создание общего ICMP-контекста/регистрацию REPLY и запуск raw exit socket.
Это две разные операции: при отказе raw нельзя уничтожать работающую клиентскую сторону.
Например, icmp_proxy_init создаёт общий контекст, отдельный enable_exit открывает
raw socket и регистрирует чтение. Новый raw socket публикуется только после полного успеха.
Обе операции возвращают ошибки непосредственно своему вызывающему коду.
Рекомендуемая политика: EPERM/EACCES/отсутствие поддержки raw явно отключают только ICMP exit,
с WARN о причине и итогом запуска tcp=on/udp=on/icmp_exit=off. TCP/UDP продолжают работать.
Ошибки выделения/регистрации обязательных ресурсов прерывают запуск proxy с полной очисткой.
Если продукт требует обязательного ICMP, отсутствие raw должно быть фатальным на уровне
конфигурации; это отдельная политика, а не скрытый fallback внутри init.
В production виртуальные echo-ответы недопустимы. Тестовый loopback выбирается явно
и должен работать одинаково с правами raw и без них.
Заданные SOCKS/HTTP listeners обязательны: enabled с пустым/неверным адресом либо
неудачный bind/listen проваливают create. Нельзя вернуть готовый клиент без выбранного входа.
Проверки router bind оставляем, но при живом inst нынешний bind не выделяет память
и возвращает 0; не нужно изображать реальные OOM-пути там, где их нет.
UDP и ограниченные ресурсы
Новый flow сначала полностью создаётся локально: socket -> nonblocking -> bind ->
read registration -> обеспечение expire timer. Только затем добавляется в список.
Любой отказ снимает уже созданные регистрации/таймеры и закрывает именно новый сокет.
Если общий таймер не удаётся продлить, оставшиеся flows закрываются с диагностикой:
состояние без гарантированной очистки не сохраняется.
sendto проверяется по точному размеру. Для нулевой датаграммы n=0 означает успех.
EINTR допускает повтор операции; EAGAIN/ENOBUFS означает явное отбрасывание датаграммы
с диагностикой. Не добавляем неограниченную очередь и TCP-подобные повторы UDP.
Остальные ошибки также логируются с peer, tuple, длиной и errno; packet-specific ошибка
не должна автоматически уничтожать рабочий flow. Невалидный/сломанный сокет удаляется.
Проверяются ошибки router send, выделения ответов и доставки в TUN.
ICMP request тоже не публикуется без рабочего механизма истечения срока ожидания.
TCP HUP, FIN и границы ответственности
FIN относится только к одному направлению и следует после его DATA. Ошибка реальной
записи остаётся ошибкой даже после FIN противоположной стороны; ENOTCONN не превращается
в успешный on_fin_sent. Существующая регрессия test_fin_shutdown_error сохраняет этот контракт.
HUP — повод обработать завершение сокета, но не разрешение обойти high_water или явную pause_read.
При достижении high_water чтение останавливается до low_water. Если level-triggered HUP
не даёт event loop уснуть, регистрацию сокета нужно временно снять и восстановить по
queue waiter; простой return с оставленным HUP в epoll/poll создаёт busy loop.
Нужен один механизм восстановления регистрации в tcp_io, учитывающий connected,
fin_remote, write_buf, write_queue и уничтожение владельца. Состояние fd/пулы сохраняются
до окончательного destroy; запланированная запись не должна зависеть от паузы чтения.
Повторное добавление использует новую generation uasync, старые события игнорируются.
Явную паузу владельца следует отличать от паузы по high_water; HUP не снимает ни одну.
Это наиболее чувствительный этап: перед реализацией проверить переходы регистрации
при HUP с ожидающей записью, low_water, обеих FIN и destroy из callback.
Не менять глобальную семантику всех регистраций uasync ради одного tcp_io-сценария.
Поздние WINDOW/FIN/CLOSE/ERROR для отсутствующего потока игнорируются как события
завершённого sid с debug-диагностикой. DATA не воскрешает поток, неверное окно живого
потока по-прежнему является ошибкой. HTTP не обязан ждать FIN для ответа с известной длиной.
Проверить синхронный loopback: состояние перед отправкой control резервируется заранее;
после callback с возможным destroy нельзя обращаться к владельцу без проверки его жизни.
HTTPS-тест разделить на нормальное двустороннее TLS shutdown и явный abrupt close.
В первом случае ожидать отсутствие EPIPE/ENOTCONN; во втором — корректную ошибку,
сохранность уже доставленного ответа и освобождение ресурсов. Proxy не разбирает TLS.
Сначала проверить эту гипотезу управляемым тестовым сервером, затем менять fixture.
Проверка после реализации
- Fault injection: socket/nonblocking/bind/read registration/expire timer/sendto/OOM.
Проверять отсутствие fd/памяти/таймеров/handlers после каждого отказа и успешный повторный запуск.
- client only, exit only, обе роли на одном instance, два независимых instance;
отказ raw сохраняет ICMP REPLY клиента, остановка одного TCP-модуля не ломает общие контексты.
- UDP payload=0, краткая/ошибочная отправка, transient error и следующий успешный пакет.
- HUP при полном proxy window и остановленном consumer: очередь остаётся ограниченной,
event loop не крутится, после снятия блокировки все байты и FIN доходят в правильном порядке.
- pending write при HUP, manual pause, low_water/re-register, destroy в callback и stale events.
- FIN в обе стороны при backpressure, RST до/после ответа, нормальный/оборванный TLS,
поздний WINDOW, синхронный loopback и HTTP keep-alive со сменой sid.
- Текущие proxy9, test_tcp_io, тесты uasync/queue, затем ASAN/UBSan и полный check.sh.
Windows/FreeBSD требуют отдельной проверки poll/select; Linux PASS её не заменяет.
Доказательства исходного анализа
Диагностический test_socks_http_proxy под gdb: PASS, 16 byte-exact загрузок.
Включены существующие proxy/socket debug через gdb, проектные файлы не менялись.
Полный журнал: /tmp/proxy-design-debug.log; события lifecycle: /tmp/proxy-design-lifecycle.log.
Системные вызовы TLS: /tmp/proxy-design-close.744427,
сводка четырёх соединений: /tmp/proxy-design-close-summary.txt.
Эти журналы относятся к реализации до исправлений.
Реализация и проверка архитектуры
1. UTUN_INSTANCE единолично создаёт и уничтожает общие UDP/ICMP-контексты. TCP-модули
владеют только своими bindings/listeners/потоками. Их rollback не затрагивает общие
контексты. Занятой binding не перезаписывается, повторный init TCP отклоняется.
Proxy закрывается и deferred free обрабатываются до остановки групп/транспорта.
2. Все заданные listeners и TUN mappings обязательны. Частичный отказ возвращает NULL
и освобождает созданные ресурсы; create_from_config сохраняет config у вызывающего.
3. ICMP init создаёт контекст/binding, enable_exit атомарно создаёт raw socket/регистрацию.
Отсутствие прав/поддержки отключает только ICMP exit с явным WARN. Клиентские REPLY
сохраняются; остальные ошибки запуска фатальны. Test loopback включается явно.
4. UDP flow публикуется после всех обязательных шагов. Отказы создания/продления таймера
не оставляют бессрочных flows. Проверяются socket/nonblocking/bind/registration,
sendto, ответные выделения/router/TUN. EINTR повторяется, нулевая датаграмма допустима;
ошибки пакета логируются без очереди повторов, сломанный сокет закрывается.
5. TCP HUP сохраняет high/low-water и ручную остановку чтения. Без доступного I/O
регистрация временно снимается; low_water восстанавливает её с новой generation.
fd/очереди сохраняются; запись обслуживается независимо. Ошибка восстановления
регистрации корректно вызывает on_error, включая destroy владельца из callback.
6. Поздний WINDOW отсутствующего sid отбрасывается с DEBUG. Формат протокола и машина
CONNECTED/WINDOW/FIN сохранены. Реальные ошибки записи/SHUT_WR не превращаются в успех.
HTTPS fixture теперь завершает TLS двусторонне; abrupt close — отдельный режим.
7. Неподдержанный enabled в серверных конфигурациях профильных и TUN-тестов заменён
поддержанным connect_timeout. Контракты заголовков и документы модулей обновлены.
Проверено:
- test_proxy_lifecycle: 25/25 изолированных сценариев, контроль fd/памяти/регистраций/таймеров.
Покрыты socket/nonblock/bind/registration/timer/OOM, отправка/EINTR/нулевой UDP,
частичный отказ instance, повторный запуск, ICMP REPLY после EPERM, HUP re-register,
ожидающая запись и destroy из error callback. /tmp/proxy-fix-lifecycle2.log.
- test_tcp_io: 19/19, HUP high_water=4 на epoll/poll, парковка без busy loop,
точная доставка данных и FIN после low_water, ручная пауза и независимая запись.
- SOCKS/HTTP/HTTPS: оба режима по 16 byte-exact загрузок. Штатный TLS shutdown без
EPIPE/ENOTCONN; abrupt сохраняет ошибки TCP без повреждения доставленных ответов.
/tmp/proxy-fix-check1/test_socks_http_proxy.log, /tmp/proxy-fix-tls-abrupt.log.
- Основной make check: 185/185 PASS. /tmp/proxy-fix-full-check2.log.
Первый check.sh остановлен общим лимитом 141с; local_transport_packets отдельно PASS.
Повтор завершил основной набор, но TUN-этапу помешал чужой процесс на UDP 15002.
- Те же TUN fixtures и идентичный src/utun выполнены в отдельном network namespace:
proxy9/9, burst1/1, load8/8 PASS; load проверил134217728 байт.
/tmp/proxy-fix-network-check.log; копия fixtures/логов: /tmp/utun-proxy-fix-network/.
- Настоящий raw ICMP echo до127.0.0.1 через два ETCP-instance и явный loopback PASS.
UTUN_TEST_ICMP_RAW=1 требует raw socket и не переключается на виртуальный ответ.
/tmp/proxy-fix-icmp-raw.log, /tmp/proxy-fix-icmp-loopback.log.
- ASAN/UBSan/LSan: 16/16 PASS с detect_leaks=1, включая оба TLS-режима, lifecycle,
tcp_io/flush, пакеты, regressions, HTTP, TCP/UDP/ICMP, services, uasync/queue и STCP.
Настоящий raw ICMP дополнительно PASS под теми же санитайзерами с root.
Все встроенные зависимости собраны с sanitizer-флагами в отдельной копии текущих
исходников /tmp/utun-proxy-fix-sanitizers; обычные объекты проекта не менялись.
/tmp/proxy-fix-sanitizer-results.log, /tmp/proxy-fix-sanitizer-icmp-raw.log.
В проверенных Linux-сценариях противоречий владения/rollback/half-close не выявлено.
Сборка и runtime Windows/FreeBSD не проверялись; Linux epoll/poll не заменяют эту проверку.
+185 -46
View File
@@ -1,61 +1,200 @@
# 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.
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
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).
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 создаётся только с рабочим таймером истечения срока ожидания.
`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 создаёт
и уничтожает 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` сверены с реализацией.
+10
View File
@@ -71,6 +71,16 @@ ConnectivityManager до запроса запуска и передаёт ко
| Группа | Сессии пиров, её маршруты и логические каналы, планировщик подключения и recovery |
| Сессия пира группы | NCD handle с момента CONNECTING, согласованные эпохи, состояние обмена таблицей, очередь отправки |
Proxy настраивается при создании ядра. Instance владеет общими UDP/ICMP-контекстами;
TCP client/server владеют только своими bindings, listeners и TCP-потоками.
Общие контексты нужны для exit либо клиентского TUN; SOCKS/HTTP без TUN их не требуют.
Ошибка обязательного listener, TUN mapping или регистрации откатывает создание instance.
Недоступность raw ICMP по правам или поддержке отключает только ICMP exit с WARN;
клиентские ICMP REPLY и TCP/UDP продолжают работать.
`utun_instance_destroy()` закрывает proxy и обрабатывает отложенные free до остановки
UTUN/CHAT-групп и транспорта. Waiters/таймеры снимаются до освобождения их владельцев.
Остановка отдельного TCP proxy-модуля не уничтожает общие UDP/ICMP-контексты.
SQLite открывается ядром один раз: `db_path/chats.db`, либо `:memory:`, если путь
не задан. Остановка чата сохраняет каналы и сообщения в этой БД. В варианте
`:memory:` данные сохраняются только до уничтожения ядра.
+20
View File
@@ -4,6 +4,26 @@
## Текущие задачи
[+] **Proxy: исправить HUP/backpressure, владение и rollback** — завершено 24.10.2026.
Согласованный проект, причины и результаты: [doc/proxy_fix_plan.txt](proxy_fix_plan.txt).
HUP больше не обходит read_high_water/ручную паузу; idle HUP снимается с poll,
low_water восстанавливает регистрацию с новой generation, запись остаётся независимой.
Общими UDP/ICMP владеет instance; TCP-модули освобождают только свои ресурсы.
Proxy закрывается до групп/транспорта. Listeners/mappings обязательны, частичный запуск откатывается.
Raw ICMP запускается отдельно: EPERM сохраняет клиентские REPLY и TCP/UDP; остальные ошибки запуска фатальны.
UDP проверяет создание flow, таймеры и точную отправку; нулевой payload и EINTR обработаны.
Поздний WINDOW завершённого sid — DEBUG drop. Неверный enabled убран из серверных тестовых конфигов.
Штатный TLS shutdown проходит без EPIPE/ENOTCONN; abrupt сохраняет реальные ошибки TCP.
Оба TLS-режима: 16 byte-exact загрузок. Настоящий raw ICMP и явный loopback PASS.
test_proxy_lifecycle: 25/25 сценариев отказов без утечек fd/памяти; test_tcp_io: 19/19, epoll/poll HUP.
Основной make check: 185/185 PASS. Общий check.sh сначала не уложился в141с,
затем TUN-этапу помешал занятый другим процессом UDP15002. TUN-тесты повторены
с тем же бинарником в отдельном network namespace: proxy9/9, burst1/1, load8/8 PASS,
134217728 проверенных байт. ASAN/UBSan/LSan: 16/16 и отдельный raw ICMP PASS, detect_leaks=1.
Логи: /tmp/proxy-fix-full-check2.log, /tmp/proxy-fix-network-check.log,
/tmp/proxy-fix-sanitizer-results.log, /tmp/proxy-fix-icmp-raw.log.
Windows/FreeBSD runtime не проверялся.
[ ] **ETCP: полезная утилизация нескольких каналов и доставка при зависании части линков** — общая задача открыта.
24.10.2026: тесты отдельно проверяют обработку RTO (опоздание<=20мс), ожидание готового повтора (<=100мс)
и доставку сверх заданной задержки канала (<=100мс). TX учитывается после принятия send.