Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
f92c78a2e0
32 files changed
+1709
-1296
No files matched your search
@@ -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
@@ -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` сверены с реализацией.
|
||||
@@ -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:` данные сохраняются только до уничтожения ядра.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user