From 04ace670ed02eed0bee3edca3ecd2b801374f056 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 24 Oct 2026 13:17:33 +0200 Subject: [PATCH] docs(proxy): design error handling and lifecycle fixes --- doc/proxy_fix_plan.txt | 137 +++++++++++++++++++++++++++++++++++++++++ doc/tasks.md | 9 +++ 2 files changed, 146 insertions(+) create mode 100644 doc/proxy_fix_plan.txt diff --git a/doc/proxy_fix_plan.txt b/doc/proxy_fix_plan.txt new file mode 100644 index 00000000..3b632837 --- /dev/null +++ b/doc/proxy_fix_plan.txt @@ -0,0 +1,137 @@ +Proxy: исправления и проверка архитектуры +Дата: 2026-10-24. Статус: проект изменений; исходники не изменены. + +Подтверждённые факты + +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. +Ресурсы закрываются в одном 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. +Это проверка существующей реализации и проекта изменений; исправленный код ещё не протестирован. diff --git a/doc/tasks.md b/doc/tasks.md index f63bcbd0..6346d550 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -5,6 +5,15 @@ ## Текущие задачи [ ] **Proxy: проверить ошибки teardown и убрать неверный ключ в тестовых конфигах** — обнаружено при аудите документации 24.10.2026. + Проект исправлений и проверка владения/rollback/half-close: [doc/proxy_fix_plan.txt](proxy_fix_plan.txt). + Уточнение 24.10.2026: существующие proxy/socket debug включены через gdb без правок исходников. + Подтверждён обход backpressure при HUP: tcp_io error_cb снимает read_waiter и принудительно читает, + очередь выросла до53 блоков при high_water=4. Нужны ограниченное чтение и ожидание low_water без HUP busy loop. + strace четырёх HTTPS-потоков: после EOF назначения приходит TLS-запись24 байта от клиента; + send принимает её, затем SO_ERROR=EPIPE или shutdown=ENOTCONN. Повторный shutdown не обнаружен. + Не подавлять эти ошибки; отдельно проверить корректный TLS shutdown тестового сервера и abrupt close. + Доказательства: /tmp/proxy-design-debug.log, /tmp/proxy-design-lifecycle.log, + /tmp/proxy-design-close.744427 и /tmp/proxy-design-close-summary.txt. Исходники пока не изменены. Девять профильных тестов PASS, но в test_socks_http_proxy после ответов встречаются EPIPE (32) в tcp_io error_cb и ENOTCONN (107) при shutdown write; причина и корректность cleanup отдельно не установлены. Побайтовая проверка всех 16 загрузок проходит. Начать с информативного лога FIN/close/socket events