docs(proxy): consolidate current protocol and audit coverage
This commit is contained in:
1 parent
f003aaa92d
commit
5e80a9cc6c
2 files changed
+175
-519
No files matched your search
+162
-46
@@ -1,61 +1,177 @@
|
||||
# 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.
|
||||
|
||||
## 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
|
||||
после освобождения владельца нет.
|
||||
|
||||
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).
|
||||
|
||||
## Проверки и ограничения
|
||||
ICMP exit использует один raw socket; заменяет id/seq token и сопоставляет
|
||||
wire_id/wire_seq/IP цели с pending. Исходные id/seq восстанавливаются для клиента.
|
||||
Порог ожидания >5 с проверяется периодически. Raw reply проходит проверку ICMP checksum.
|
||||
Нечётный хвост checksum обрабатывается без чтения за буфером.
|
||||
test_loopback даёт виртуальный ответ только при отсутствии raw socket.
|
||||
Успешный ICMP init не гарантирует raw socket: его ошибка оставляет контекст активным.
|
||||
|
||||
`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,
|
||||
клиент делает это при наличии TUN. Сначала уничтожается клиент, затем сервер:
|
||||
клиент сохраняет общие UDP/ICMP пока сервер включён, server destroy уничтожает их.
|
||||
После удаления exit-потоков ua должен обработать отложенные free.
|
||||
Ошибки listener/bind/подмодулей не всегда пробрасываются init/create — точные контракты в .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_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 загрузки |
|
||||
| test_udp_proxy / test_icmp_proxy | Два instance через ETCP; ICMP использует raw socket либо тестовый fallback |
|
||||
| test_tcp_io_flush | Callback после последнего синхронного send |
|
||||
|
||||
Аудит 2026-10-24: перечисленные девять тестовых программ пересобраны и прошли на Linux.
|
||||
Полный check.sh, реальные TUN/load/burst и Windows/FreeBSD в этом аудите не запускались.
|
||||
Создание proxy из конфигурации в utun_instance.c исключено под _WIN32.
|
||||
IPv4-фрагменты на входе proxy TUN отклоняются; сборки фрагментов нет.
|
||||
Описания модулей: `src/proxy/*_doc.md`; все семь заголовков `src/proxy/*.h` сверены с реализацией.
|
||||
+13
-473
@@ -1,478 +1,18 @@
|
||||
## TCP Proxy Protocol — архитектура (v2, без CONNECTED)
|
||||
# Протокол proxy
|
||||
|
||||
### 1. Протокольный формат (общий для client и server)
|
||||
Актуальное описание TCP/UDP/ICMP, router-метаданных, окон, half-close и проверок:
|
||||
[doc/proxy_protocol.md](../../doc/proxy_protocol.md).
|
||||
|
||||
```
|
||||
┌──────┬──────┬───────────────────────┬──────────────────────┐
|
||||
│svc_id│subcmd│ stream_id (4) │ data ... │
|
||||
│ 1B │ 1B │ │ │
|
||||
└──────┴──────┴───────────────────────┴──────────────────────┘
|
||||
HDR = 6 байт
|
||||
TCP ожидает реальный CONNECTED и использует WINDOW; FIN завершает одно направление.
|
||||
Прежние схемы без CONNECTED, с общим TCP service ID и 9-байтовым CLOSE_ALL не соответствуют
|
||||
реализации. Общие определения и контракт proxy_flow находятся в [proxy_protocol.h](proxy_protocol.h).
|
||||
|
||||
subcmd:
|
||||
0x01 CONNECT client→server : dest_ip(4)+dest_port(2)
|
||||
0x03 DATA ↔ bidirectional: payload
|
||||
0x04 CLOSE ↔ bidirectional: (нет данных)
|
||||
0x05 ERROR ↔ bidirectional: (нет данных)
|
||||
```
|
||||
Описание отдельных модулей:
|
||||
|
||||
---
|
||||
- [TCP client](tcp_proxy_client_doc.md) — TUN/lwIP, маршрутизация к exit, приём ответов.
|
||||
- [TCP server](tcp_proxy_server_doc.md) — OS connect, окна, очереди, FIN/CLOSE/ERROR.
|
||||
- [SOCKS и HTTP](socks_proxy_doc.md) — listeners, DNS, HTTP framing/keep-alive/туннели.
|
||||
- [UDP](udp_proxy_doc.md) — per-instance flows и восстановление ответа в TUN.
|
||||
- [ICMP](icmp_proxy_doc.md) — raw socket, сопоставление echo, тестовый fallback.
|
||||
|
||||
### 2. Диаграмма состояний `tcp_proxy_client_conn`
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ ALLOC + │ accept_cb: u_calloc, stream_id++
|
||||
│ CONNECT │ send CONNECT → exit
|
||||
│ отправлен │
|
||||
└──────┬───────┘
|
||||
│
|
||||
┌───────────┴───────────┐
|
||||
│ send успешен? │
|
||||
└───────────┬───────────┘
|
||||
Y │ N
|
||||
┌────────┘ └──────┐
|
||||
▼ ▼
|
||||
┌────────────┐ ┌──────────────┐
|
||||
│ ACTIVE │ │ FREE │
|
||||
│ traffic │ └──────────────┘
|
||||
└────┬──┬────┘
|
||||
│ │
|
||||
│ └─────────────────────────────────┐
|
||||
│ │
|
||||
▼ lwIP FIN (p=NULL) ▼ ERROR от lwIP/exit
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ TUN_CLOSED │ │ ERROR │
|
||||
│ tun_closed=1 │ │ error=1 │
|
||||
│ ↓ send_close │ │ send ERROR→ │
|
||||
│ после drain │ └──────┬───────┘
|
||||
│ tx_buf+lwIP │ │
|
||||
└──────┬───────┘ poll_cb: tcp_abort+free
|
||||
│
|
||||
│ recv CLOSE от exit
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ TUN+REM CLOSED │
|
||||
│ tun=1 rem=1 │
|
||||
│ ждём flush буферов│
|
||||
└────────┬──────────┘
|
||||
│ poll_cb: pending=0, unsent=NULL, unacked=NULL
|
||||
│ (или sndbuf==0)
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ CLEANUP │
|
||||
│ free(pc) │
|
||||
└──────────────┘
|
||||
|
||||
|
||||
ДОП. СОСТОЯНИЯ:
|
||||
┌─────────────────┐ send_data в ETCP вернул -1 (send_q полон)
|
||||
│ TX_BACKPRESSURE│ не tcp_recved() → окно lwIP=0 → recv_cb остановлен
|
||||
│ tx_buf != NULL │ etcp_router_on_send_ready() → ждём освобождения
|
||||
│ tx_waiter pend │ waiter_cb: retry send_data → tcp_recved → окно открыто
|
||||
└─────────────────┘
|
||||
|
||||
┌─────────────────┐ send_close/send_error вернул -1
|
||||
│ CLOSE_PENDING │ → retry в poll_cb
|
||||
│ close_pending=1│
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Диаграмма состояний `tcp_proxy_server_conn`
|
||||
|
||||
```
|
||||
┌───────────────────┐
|
||||
│ CREATING │ recv CONNECT
|
||||
│ socket() создан │ → неблокирующий connect()
|
||||
│ sock = неблок. │
|
||||
└────────┬──────────┘
|
||||
│
|
||||
┌────────────┴────────────┐
|
||||
│ connect() результат? │
|
||||
└────┬──────────┬──────────┘
|
||||
EINPROGRESS│ │ошибка сразу
|
||||
▼ ▼
|
||||
┌──────────┐ ┌──────────────┐
|
||||
│CONNECTING│ │ ERROR │ send ERROR→client
|
||||
│ждём write│ │ FREE │
|
||||
└────┬─────┘ └──────────────┘
|
||||
│ write_cb: SO_ERROR==0
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ CONNECTED │ connected=1
|
||||
│ flush pend │ → flush pending_buf в сокет
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
│ │ │
|
||||
client→DATA │ │ ERROR (sock/pipe)
|
||||
(sock_send) │ ▼
|
||||
или pending_buf │ ┌──────────────┐
|
||||
│ │ │ ERROR │ send ERROR→client
|
||||
│ │ │ FREE │
|
||||
│ sock EOF (n=0) │ │
|
||||
│ │ └──────────────┘
|
||||
│ ▼
|
||||
│ ┌──────────────────┐
|
||||
│ │ SOCK_CLOSED │ sock_closed=1
|
||||
│ │ out_buf/pause? │ если очереди пусты → send_close
|
||||
│ │ ждём drain │ иначе ждём retry_cb/waiter_cb
|
||||
│ └────────┬─────────┘
|
||||
│ │ очереди опустошены → send_close
|
||||
│ │ если cli_closed → FREE
|
||||
│ │
|
||||
client→CLOSE │
|
||||
│ │
|
||||
▼ │
|
||||
┌──────────────┐ │
|
||||
│ CLI_CLOSED │ │ cli_closed=1
|
||||
│ out_buf pend? │◄────────┘ если out_buf пуст → shutdown(SHUT_WR)
|
||||
│ ждём flush │ иначе ждём sock_retry_cb → shutdown
|
||||
└──────┬───────┘
|
||||
│
|
||||
└──────────────┐
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ cli && sock │ оба закрыты + очереди пусты
|
||||
│ очереди пусты │
|
||||
└────────┬─────────┘
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ FREE │ conn_free(rc)
|
||||
└──────────────┘
|
||||
|
||||
|
||||
ДОП. СОСТОЯНИЯ:
|
||||
┌─────────────────┐ send_msg в ETCP вернул -1
|
||||
│ PAUSED │ буферизуем в pause_buf, убираем read_id (пауза чтения)
|
||||
│ pause_buf>0 │ etcp_router_on_send_ready() → ждём освобождения
|
||||
│ pause_waiter │ waiter_cb: retry send_msg → resume read_id
|
||||
└─────────────────┘
|
||||
|
||||
┌─────────────────┐ CLOSE/ERROR не доставлен через ETCP
|
||||
│ CLOSE_PENDING │ close_timer с backoff 50..5000 tb → ретрай
|
||||
│ close_pending=1│
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. DATA FLOW
|
||||
|
||||
```
|
||||
══════════════════════════════════════════════════════════════════════
|
||||
APP → lwIP → recv_cb → ETCP → handle → socket → DEST
|
||||
APP ← lwIP ← tcp_write ← ETCP ← read_cb ← socket ← DEST
|
||||
══════════════════════════════════════════════════════════════════════
|
||||
|
||||
CLIENT (узел A) SERVER (узел B)
|
||||
|
||||
APP (через TUN) DEST (реальный)
|
||||
│ │
|
||||
▼ TCP SYN/ACK (lwIP) ▼ TCP SYN/ACK (OS)
|
||||
┌──────────┐ ┌──────────┐
|
||||
│ lwIP │ │ socket │
|
||||
│ TCP │ │ (неблок) │
|
||||
└────┬─────┘ └────┬─────┘
|
||||
│ recv_cb │ read_cb
|
||||
│ │
|
||||
┌────▼────────────┐ ┌────▼───────────────┐
|
||||
│ tcp_proxy │ ETCP │ tcp_proxy │
|
||||
│ _client │◄═══════════════════════►│ _server │
|
||||
│ │ │ │
|
||||
│ ┌────────────┐ │ │ ┌──────────────┐ │
|
||||
│ │ recv_cb │ │──CONNECT+DATA──► │ │handle_connect│ │
|
||||
│ │(lwIP→ETCP) │ │ │ │handle_data │ │
|
||||
│ │ │ │ backpressure: │ └──────┬───────┘ │
|
||||
│ │ send fail: │ │ waiter на send_q │ │ sock_send │
|
||||
│ │ tx_buf+ │ │ │ ┌──────▼───────┐ │
|
||||
│ │ waiter │ │ │ │ out_buf │ │
|
||||
│ │ окно lwIP=0│ │ │ │ pending_buf │ │
|
||||
│ └────────────┘ │ │ └─────────────┘ │
|
||||
│ │ │ │ send() │
|
||||
│ ┌────────────┐ │ │ ┌──────▼───────┐ │
|
||||
│ │handle_data │ │◄────DATA──────── │ │ read_cb │ │
|
||||
│ │handle_close│ │ │ │(sock→ETCP) │ │
|
||||
│ └─────┬──────┘ │ │ │ │ │
|
||||
│ │ │ │ │ send fail: │ │
|
||||
│ ┌─────▼──────┐ │ │ │ pause_buf+ │ │
|
||||
│ │ to_lwip │ │ │ │ waiter+ │ │
|
||||
│ │ (очередь) │ │ │ │ пауза read_id│ │
|
||||
│ └─────┬──────┘ │ │ └─────────────┘ │
|
||||
│ │tcp_write│ │ │
|
||||
│ ▼ │ └───────────────────┘
|
||||
│ lwIP→TUN→APP │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. BACKPRESSURE (lwIP→ETCP и socket→ETCP)
|
||||
|
||||
```
|
||||
══════════════════════════════════════════════════════════════════════
|
||||
|
||||
Используется встроенный механизм ll_queue threshold waiter (на router send_q):
|
||||
etcp_router_on_send_ready(inst, group_id, node_id, svc_id, &waiter, cb, arg)
|
||||
→ внутри: queue_waiter_wait(rconn->send_q, &waiter)
|
||||
etcp_router_cancel_send_ready(inst, group_id, node_id, svc_id, &waiter)
|
||||
|
||||
CLIENT (lwIP → ETCP):
|
||||
recv_cb(buf, len):
|
||||
ret = send_data(buf, len)
|
||||
if ret == 0: tcp_recved(len)
|
||||
if ret < 0:
|
||||
tx_buf = copy(buf), tx_len = len
|
||||
НЕ tcp_recved() → окно lwIP = 0, recv_cb больше не вызывается
|
||||
etcp_router_on_send_ready(inst, group_id, via_node_id, ETCP_RT_ID_TCP_PROXY_SERVER, &tx_waiter, tx_waiter_cb, pc)
|
||||
|
||||
tx_waiter callback:
|
||||
ret = send_data(tx_buf, tx_len)
|
||||
if ret == 0:
|
||||
tcp_recved(tx_len) → окно открыто, lwIP возобновляет recv_cb
|
||||
free(tx_buf)
|
||||
if tun_closed → send_close()
|
||||
// если ret < 0 → waiter остаётся, router вызовет снова
|
||||
|
||||
SERVER (socket → ETCP):
|
||||
read_cb(buf, n):
|
||||
ret = send_msg(DATA, buf, n)
|
||||
if ret == 0: ok
|
||||
if ret < 0:
|
||||
pause_buf = copy(buf), pause_len = n
|
||||
uasync_remove_socket_t(sock) → пауза чтения с сокета
|
||||
etcp_router_on_send_ready(inst, group_id, peer_node_id, ETCP_RT_ID_TCP_PROXY_CLIENT, &pause_waiter, pause_waiter_cb, rc)
|
||||
|
||||
pause_waiter callback:
|
||||
ret = send_msg(DATA, pause_buf, pause_len)
|
||||
if ret == 0:
|
||||
free(pause_buf)
|
||||
uasync_add_socket_t(sock) → возобновляем чтение
|
||||
if sock_closed → send_close()
|
||||
// если ret < 0 → waiter остаётся
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Последовательность: HANDSHAKE (упрощённый, без CONNECTED)
|
||||
|
||||
```
|
||||
CLIENT (lwIP) CLIENT PROXY ETCP SERVER PROXY SOCKET
|
||||
─────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
tcp_proxy_client_accept_cb() CONNECT
|
||||
├─ u_calloc(conn) ────────────────────────────→ tcp_proxy_server_handle_connect()
|
||||
├─ stream_id++ ├─ socket(AF_INET,SOCK_STREAM)
|
||||
├─ send_connect() ├─ socket_set_nonblocking()
|
||||
│ [dest_ip(4)+dest_port(2)] ├─ uasync_add_socket_t(read/write/error)
|
||||
├─ conn→conns list ├─ connect(неблок.) → EINPROGRESS
|
||||
│ └─ rc→conns list
|
||||
▼
|
||||
ACTIVE — данные идут СРАЗУ (без ожидания CONNECTED)
|
||||
│
|
||||
│ DATA ...connect завершён...
|
||||
├──────────────────────────────────────────────────→ tcp_proxy_server_sock_write_cb()
|
||||
│ ├─ connected=1
|
||||
│ DATA └─ flush pending_buf в сокет
|
||||
├──────────────────────────────────────────────────→
|
||||
│
|
||||
│ DATA ←─ tcp_proxy_server_sock_read_cb()
|
||||
│◄────────────────────────────────────────────── (socket recv → ETCP)
|
||||
│
|
||||
└──→ трафик идёт в обе стороны
|
||||
|
||||
При ошибке connect:
|
||||
...connect fail... → tcp_proxy_server_send_error() → client получает ERROR, error=1, cleanup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Последовательность: CLOSE (drain очередей → close в конце)
|
||||
|
||||
```
|
||||
CLIENT (lwIP) CLIENT PROXY ETCP SERVER PROXY SOCKET
|
||||
─────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
══ Вариант А: lwIP закрывает первым ══
|
||||
|
||||
tcp_proxy_client_recv_cb(p=NULL)
|
||||
├─ tun_closed=1
|
||||
│ если tx_buf пуст → send_close CLOSE
|
||||
│ если tx_buf pend → waiter досылает ──────────→ tcp_proxy_server_handle_close()
|
||||
│ потом send_close ├─ cli_closed=1
|
||||
└─ │ если out_buf пуст → shutdown(SHUT_WR)
|
||||
│ иначе ждём sock_retry_cb
|
||||
│
|
||||
...out_buf дослан...
|
||||
sock_retry_cb:
|
||||
shutdown(SHUT_WR)
|
||||
│
|
||||
...сокет дочитывает остаток...
|
||||
│
|
||||
tcp_proxy_server_sock_read_cb(n=0)
|
||||
├─ sock_closed=1
|
||||
│ если pause_buf пуст → send_close
|
||||
│ иначе ждём waiter_cb
|
||||
│
|
||||
CLOSE │
|
||||
tcp_proxy_client_handle_close() ◄────────────────────────┘
|
||||
└─ rem_closed=1
|
||||
│
|
||||
poll_cb: tun=1, rem=1, to_lwip пуст, unsent=NULL, unacked=NULL
|
||||
├─ tcp_close(pcb)
|
||||
└─ conn_free()
|
||||
|
||||
|
||||
══ Вариант Б: серверный сокет закрывается первым ══
|
||||
|
||||
tcp_proxy_server_sock_read_cb(n=0)
|
||||
├─ sock_closed=1
|
||||
│ если out_buf+pause_buf пусты → send_close
|
||||
│ иначе ждём retry/waiter
|
||||
CLOSE │
|
||||
tcp_proxy_client_handle_close() ◄────────────────────────┘
|
||||
└─ rem_closed=1
|
||||
│
|
||||
▼
|
||||
...приложение закрывает TCP...
|
||||
│
|
||||
tcp_proxy_client_recv_cb(p=NULL)
|
||||
├─ tun_closed=1
|
||||
│ если tx_buf пуст → send_close
|
||||
└─
|
||||
|
||||
poll_cb: оба закрыты, буферы пусты → cleanup
|
||||
|
||||
|
||||
══ Вариант В: Ошибка / аномалия ══
|
||||
|
||||
ERROR
|
||||
─────────────────────── (или ←) ──────────
|
||||
error=1
|
||||
│
|
||||
▼
|
||||
poll_cb: tcp_abort(pcb) → немедленная очистка
|
||||
или: rc→error=1 → conn_free
|
||||
|
||||
|
||||
══ Вариант Г: conn не найден / closed / error ══
|
||||
|
||||
Любой пришедший пакет (DATA/CLOSE/ERROR) для conn в закрытом состоянии:
|
||||
→ tcp_proxy_*_send_msg(ERROR, stream_id)
|
||||
→ удалённая сторона получает ERROR и немедленно закрывается
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. Диспетчеризация в `tcp_proxy_client_etcp_recv_cb()`
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ tcp_proxy_client_etcp_recv_cb() │
|
||||
│ (единая точка входа для обоих направлений) │
|
||||
├────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ subcmd == CONNECT? │
|
||||
│ └── tcp_proxy_server_handle_connect() ←── всегда сервер │
|
||||
│ │
|
||||
│ stream_id в server.conns? (если server.enabled) │
|
||||
│ ├── DATA → tcp_proxy_server_handle_data() │
|
||||
│ ├── CLOSE → tcp_proxy_server_handle_close() │
|
||||
│ └── ERROR → rc->error=1; conn_free() │
|
||||
│ │
|
||||
│ stream_id в client.conns? (если client proxy != NULL) │
|
||||
│ ├── DATA → tcp_proxy_client_handle_data() │
|
||||
│ ├── CLOSE → tcp_proxy_client_handle_close() │
|
||||
│ └── ERROR → tcp_proxy_client_handle_error() │
|
||||
│ │
|
||||
│ Приоритет: │
|
||||
│ 1. CONNECT всегда интерпретируется сервером │
|
||||
│ 2. Если server.enabled И stream_id найден в server.conns → server │
|
||||
│ 3. Иначе если client proxy != NULL → client │
|
||||
│ 4. Иначе drop │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. INIT
|
||||
|
||||
```
|
||||
CLIENT SERVER
|
||||
─────────────────────────────────────────────────────────
|
||||
|
||||
utun_instance.c
|
||||
│
|
||||
├─ tcp_proxy_client_create() tcp_proxy_server_init()
|
||||
│ ├─ tun_init_nat() (TUN iface) ├─ memset(ctx,0)
|
||||
│ ├─ lwip_tcp_init() (TCP стек) ├─ ctx->enabled = config ...
|
||||
│ ├─ etcp_router_bind( ├─ etcp_router_bind(
|
||||
│ │ ETCP_ID_TCP_PROXY, │ ETCP_ID_TCP_PROXY,
|
||||
│ │ tcp_proxy_client_etcp_recv_cb) │ tcp_proxy_client_etcp_recv_cb)
|
||||
│ ├─ udp_proxy_init() ├─ udp_proxy_init()
|
||||
│ └─ icmp_proxy_init() └─ icmp_proxy_init() (если client не активен)
|
||||
│
|
||||
└─ ОБА слушают ETCP_ID_TCP_PROXY через один обработчик
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10. Поля структур
|
||||
|
||||
```
|
||||
tcp_proxy_client_conn:
|
||||
stream_id — уникальный ID потока
|
||||
tun_closed — lwIP/FIN (сторона TUN)
|
||||
rem_closed — CLOSE от exit
|
||||
error — немедленная очистка
|
||||
close_sent — CLOSE/ERROR отправлен
|
||||
close_pending — отправка не удалась, ретрай
|
||||
to_lwip — очередь DATA от exit → lwIP (feed_from_transport)
|
||||
pcb — lwIP tcp_pcb
|
||||
tx_buf — буфер при backpressure lwIP→ETCP (send_data fail)
|
||||
tx_len — длина tx_buf
|
||||
tx_waiter — waiter на router send_q (etcp_router_on_send_ready)
|
||||
|
||||
tcp_proxy_server_conn:
|
||||
stream_id — уникальный ID потока
|
||||
cli_closed — клиент прислал CLOSE
|
||||
sock_closed — сокет получил EOF
|
||||
error — ошибка
|
||||
connected — сокет подключился (connect завершён)
|
||||
close_pending — CLOSE/ERROR не доставлен, ретрай
|
||||
pending_buf — буфер данных от клиента до завершения connect
|
||||
out_buf — буфер отправки в сокет (неблокирующий send, EAGAIN retry)
|
||||
out_off/out_len — offset/размер out_buf
|
||||
out_timer — таймер повтора send в сокет
|
||||
out_backoff — backoff для retry send в сокет
|
||||
pause_buf — буфер при backpressure socket→ETCP (send_msg fail)
|
||||
pause_len — длина pause_buf
|
||||
pause_waiter — waiter на router send_q (etcp_router_on_send_ready)
|
||||
close_timer — таймер повтора CLOSE/ERROR
|
||||
close_backoff — backoff для retry CLOSE/ERROR
|
||||
sock — OS сокет к адресату
|
||||
read_id — handle uasync для событий сокета
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 11. etcp_router waiter API
|
||||
|
||||
```
|
||||
// etcp_router.h
|
||||
void etcp_router_on_send_ready(struct UTUN_INSTANCE* inst, uint64_t group_id,
|
||||
uint64_t node_id, uint8_t svc_id, struct queue_waiter_handle* h,
|
||||
queue_threshold_callback_fn cb, void* arg);
|
||||
void etcp_router_cancel_send_ready(struct UTUN_INSTANCE* inst, uint64_t group_id,
|
||||
uint64_t node_id, uint8_t svc_id, struct queue_waiter_handle* h);
|
||||
|
||||
// Внутри:
|
||||
// rconn = router_conn_find(inst, group_id, node_id, svc_id)
|
||||
// queue_waiter_wait(rconn->send_q, h, cb, arg)
|
||||
// queue_waiter_cancel(rconn->send_q, h)
|
||||
//
|
||||
// send_q — очередь router conn при полном inflight
|
||||
// (threshold = ROUTER_MAX_SEND_Q_PACKETS - 1 в router_conn_reset)
|
||||
```
|
||||
Контракты функций, буферов и полей HTTP codec — в [http_proxy.h](http_proxy.h).
|
||||
Reference in new issue
Block a user