Files
utun2/doc/proxy_fix_plan.txt
T

144 lines
15 KiB
Plaintext
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: исправления и проверка архитектуры
Дата: 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.
При 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.
Это проверка существующей реализации и проекта изменений; исправленный код ещё не протестирован.