docs(proxy): design error handling and lifecycle fixes
This commit is contained in:
1 parent
0688ffc26d
commit
04ace670ed
2 files changed
+146
No files matched your search
@@ -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.
|
||||
Это проверка существующей реализации и проекта изменений; исправленный код ещё не протестирован.
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user