diff --git a/src/proxy/icmp_proxy.h b/src/proxy/icmp_proxy.h index cdef2e14..504447d6 100644 --- a/src/proxy/icmp_proxy.h +++ b/src/proxy/icmp_proxy.h @@ -1,7 +1,9 @@ /* IPv4 ICMP echo через UTUN-router: exit отправляет ping через raw socket, сопоставляет reply - * по собственному wire id/seq и восстанавливает исходные id/seq клиента. Запросы ограничены таймаутом. - * init/destroy и callbacks — в uasync; контекст принадлежит inst, роль берётся из tcp_proxy_server.enabled. - * test_loopback возвращает виртуальные ответы без сетевой отправки и предназначен для тестов. */ + * по wire id/seq и IP цели, восстанавливая исходные id/seq клиента. Запросы ограничены таймаутом. + * init/destroy и callbacks — в uasync; inst владеет контекстом в inst->icmp_proxy. + * init создаёт raw socket, если server.enabled; recv_cb проверяет текущий server.enabled для REQUEST + * и via_node_id клиентского proxy для REPLY. IP/id/seq в API — network byte order. + * test_loopback возвращает виртуальные ответы только при отсутствии raw socket; флаг его не отключает. */ #ifndef ICMP_PROXY_H #define ICMP_PROXY_H @@ -23,59 +25,70 @@ struct ETCP_CONN; #define ICMP_PROXY_SUBCMD_REQUEST 0x01 // client→exit: пингани dst_ip #define ICMP_PROXY_SUBCMD_REPLY 0x02 // exit→client: echo reply -// Заголовок сообщения на отправке (включая байт svc_id; src/dst node_id добавляет роутер): +// Сообщение для etcp_route_send (не полный wire packet; метаданные приёма добавляет router): // svc_id(1) + subcmd(1) + dst_ip(4) + orig_src_ip(4) + icmp_id(2) + icmp_seq(2) + payload #define ICMP_PROXY_HDR_SIZE 14 // Полный заголовок на приёме (после router_deliver): -// svc_id(1) + src_node_id(8) + dst_node_id(8) + subcmd(1) + dst_ip(4) + orig_src_ip(4) + icmp_id(2) + icmp_seq(2) -#define ICMP_PROXY_RECV_HDR_SIZE (ROUTER_SVC_PAYLOAD_OFF + 13) // 30 +// router header(26: svc/src/dst/flags/group) + subcmd(1) + dst_ip(4) + orig_src_ip(4) + icmp_id(2) + icmp_seq(2) +#define ICMP_PROXY_RECV_HDR_SIZE (ROUTER_SVC_PAYLOAD_OFF + 13) // 39 // Активный ICMP echo запрос (сторона exit узла) struct icmp_request { - struct icmp_request* next; - uint64_t client_node_id; - uint32_t dst_ip; + struct icmp_request* next; // следующий ожидающий запрос exit + uint64_t client_node_id; // ingress-узел для обратной доставки + uint32_t dst_ip; // IP цели ping, network byte order uint32_t orig_src_ip; // IP исходного отправителя (чтобы вернуть reply правильному адресату) - uint16_t echo_id; - uint16_t echo_seq; - uint16_t wire_id; - uint16_t wire_seq; - uint8_t payload[1500]; - size_t payload_len; - uint64_t sent_tb; - socket_t sock; // raw сокет (только если per-request) + uint16_t echo_id; // исходный id приложения, network byte order + uint16_t echo_seq; // исходный seq приложения, network byte order + uint16_t wire_id; // заменённый id в raw ICMP, network byte order + uint16_t wire_seq; // заменённый seq, уникален вместе с wire_id и dst_ip среди pending + uint8_t payload[1500]; // сохранённый префикс запроса, не используется для сборки ответа + size_t payload_len; // длина сохранённого префикса, не больше sizeof(payload) + uint64_t sent_tb; // время отправки, tb = 0.1 мс + socket_t sock; // не используется; запросы идут через общий ctx->raw_sock }; struct icmp_proxy_ctx { - int initialized; - int is_exit; - struct UTUN_INSTANCE* inst; - struct UASYNC* ua; + int initialized; // init завершён, binding запрошен + int is_exit; // снимок server.enabled при init (определяет создание raw socket) + struct UTUN_INSTANCE* inst; // заимствованный instance-владелец + struct UASYNC* ua; // заимствованный event loop socket_t raw_sock; // один SOCK_RAW для всего ICMP на exit - void* raw_read_id; - struct icmp_request* pending; - uint32_t next_token; - uint64_t request_timeout_tb; - int test_loopback; // 1 = виртуальный loopback без raw сокета (только тесты) + void* raw_read_id; // регистрация raw_sock в ua + struct icmp_request* pending; // собственные запросы, ожидающие ответа + uint32_t next_token; // генератор wire id/seq со случайным начальным значением + uint64_t request_timeout_tb; // период проверки и порог ожидания, default=50000 (5 с) + int test_loopback; // fallback при raw_sock==SOCKET_INVALID, только для тестов void* expire_timer; // периодический таймер очистки истёкших запросов }; +// Создать контекст и bind ICMP service; повторный init не меняет существующий контекст и даёт 0. +// inst/ua живут до destroy. -1 = нет inst/памяти/случайного token, 0 = контекст создан. +// 0 не гарантирует raw socket на exit: ошибка socket/регистрации не проваливает init; bind не проверяется. int icmp_proxy_init(struct UTUN_INSTANCE* inst, struct UASYNC* ua); +// Снять binding, отменить таймер, закрыть raw socket, удалить pending и обнулить inst->icmp_proxy; NULL допустим. void icmp_proxy_destroy(struct UTUN_INSTANCE* inst); -// Единый etcp_router коллбэк +// Router callback: conn->instance задаёт владельца; REQUEST требует server.enabled, REPLY — настроенного exit. +// Принимает владение entry/dgram при любом исходе, в том числе при отклонении пакета. void icmp_proxy_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry); -// Сторона клиента: принять IP/ICMP echo request с TUN, отправить через etcp +// Скопировать payload (0..65507) в REQUEST к exit в UTUN, force=0/mode=0; не требует локального контекста. +// dst_ip — цель ping, orig_src_ip — адрес приложения; payload заимствован, при длине>0 не NULL. +// 0 = принято router, <0 = ошибка; это не подтверждение raw send или получения echo reply. int icmp_proxy_send_to_exit(struct UTUN_INSTANCE* inst, uint64_t exit_node_id, uint32_t dst_ip, uint32_t orig_src_ip, uint16_t echo_id, uint16_t echo_seq, const uint8_t* payload, size_t payload_len); +// Собрать Echo Reply с IPv4 src_ip→dst_ip и исходными id/seq; пересчитать обе checksum. +// Требует inst->tcp_proxy_client->tun, payload заимствован (0..65506); 0 = в tun input_queue, -1 = ошибка. int icmp_proxy_deliver_reply(struct UTUN_INSTANCE* inst, uint32_t dst_ip, uint32_t src_ip, uint16_t echo_id, uint16_t echo_seq, const uint8_t* payload, size_t payload_len); +// Изменить тестовый fallback существующего контекста; без контекста ничего не делает. +// При наличии raw socket реальные echo-запросы имеют приоритет независимо от enabled. void icmp_proxy_set_test_loopback(struct UTUN_INSTANCE* inst, int enabled); diff --git a/src/proxy/icmp_proxy_doc.md b/src/proxy/icmp_proxy_doc.md index e43f78c5..d2bbc085 100644 --- a/src/proxy/icmp_proxy_doc.md +++ b/src/proxy/icmp_proxy_doc.md @@ -1,77 +1,64 @@ -# ICMP Proxy (icmp_proxy) +# ICMP proxy -## 1. Назначение -Проксирование ICMP Echo (ping) между клиентским и exit-узлом по протоколу ETCP. Клиент -принимает ICMP Echo Request с TUN-интерфейса, упаковывает запрос и отправляет через -`etcp_route_send` на exit. Exit-узел через нативный `SOCK_RAW`-сокет отправляет ICMP Echo -во внешнюю сеть, сопоставляет ответы по `echo_id`/`echo_seq` и возвращает клиенту. Клиент -восстанавливает IP/ICMP Echo Reply и доставляет в TUN. +`icmp_proxy.c/h` передаёт IPv4 Echo Request/Reply через `ETCP_RT_ID_ICMP_PROXY` +в `TOPO_GROUP_UTUN`. Общий формат router описан в +[doc/proxy_protocol.md](../../doc/proxy_protocol.md), контракты API — в +[icmp_proxy.h](icmp_proxy.h). Другие типы ICMP модуль не проксирует. -Один экземпляр контекста на процесс (глобальный `g_icmp_ctx`). Режим определяется флагом -`inst->tcp_proxy_server.enabled`. Для тестов предусмотрен loopback-режим без raw-сокета. +## Контекст и raw socket -## 2. Как пользоваться +Каждый `UTUN_INSTANCE` владеет отдельным `inst->icmp_proxy`, глобального контекста нет. +`init(inst, ua)` задаёт таймаут 50000 tb (5 с), случайное начало `next_token` и +сохраняет `server.enabled` в `is_exit`. Для exit открывает один неблокирующий +`AF_INET/SOCK_RAW/IPPROTO_ICMP` и регистрирует его чтение в uasync. -### Инициализация -```c -icmp_proxy_init(inst, ua); // exit создаёт SOCK_RAW, клиент — нет -``` -При инициализации регистрируется коллбэк `icmp_proxy_recv_cb` в `etcp_router` для сервиса -`ETCP_RT_ID_ICMP_PROXY`. На exit создаётся один `SOCK_RAW(IPPROTO_ICMP)` на весь модуль. +Повторный init сохраняет существующий контекст и возвращает 0. Ошибка создания +контекста или случайного token возвращает -1. Ошибка raw socket/регистрации +оставляет контекст без рабочего сокета, но init возвращает 0 и запрашивает router binding; +результат bind также не пробрасывается. Поэтому успешный init сам по себе не подтверждает +возможность отправки реального ping. Raw socket требует соответствующих прав ОС. -### Клиент: отправка ping-запроса -```c -icmp_proxy_send_to_exit(inst, exit_node_id, dst_ip, orig_src_ip, echo_id, echo_seq, payload, len); -``` -Вызывается из обработчика TUN-трафика. Запрос упаковывается и отправляется через `etcp_route_send`. -Ответ приходит асинхронно в `icmp_proxy_recv_cb` (REPLY), где вызывается `icmp_proxy_deliver_reply` -для сборки IP/ICMP Echo Reply и записи в TUN. +`test_loopback` по умолчанию выключен. `icmp_proxy_set_test_loopback(inst, 1)` разрешает +виртуальный REPLY **только при отсутствии raw socket**; открытый сокет имеет приоритет, +флаг его не отключает. Без контекста setter ничего не делает. -### Тестовый loopback -```c -icmp_proxy_set_test_loopback(inst, 1); // exit отвечает эмуляцией без raw-сокета -``` -В этом режиме exit не отправляет реальный ICMP Echo, а сразу возвращает REPLY клиенту — полезно -для тестирования прохождения пакетов через ETCP без необходимости в SOCK_RAW и внешней сети. +## Запрос и сопоставление ответа -### Очистка -```c -icmp_proxy_destroy(inst); // закрывает raw-сокет, удаляет pending-запросы, unbind -``` +REQUEST принимается при текущем `server.enabled`. Exit заменяет исходные echo id/seq +32-битным возрастающим token: старшие 16 бит — wire_id, младшие — wire_seq (в сетевом порядке). +При совпадении `wire_id/wire_seq/dst_ip` с pending выбирает следующий token. +Сопоставление ответа использует эту тройку; исходные id/seq и адрес приложения хранятся +в `icmp_request`. Разные клиенты с одинаковыми исходными id/seq не смешиваются. -### Нюансы -- Запросы истекают через 5 секунд (`ICMP_TIMEOUT_TB`). Таймер `expire_timer` запускается - при добавлении первого запроса и перезапускается после каждой очистки. -- Неопознанные echo-ответы (не найденные в pending) логируются как `DEBUG_WARN` и игнорируются. -- Контрольная сумма ICMP пересчитывается на exit (при отправке) и на клиенте (при сборке ответа). -- Поле `orig_src_ip` в протоколе нужно для восстановления исходного dst_ip на клиенте. +Raw reader проверяет IPv4 IHL/протокол, Echo Reply type/code и ICMP checksum, ищет pending +по wire id/seq и адресу отправителя, удаляет найденный запрос и отправляет REPLY ingress-узлу. +Данные ответа берутся из raw пакета. Сохранённый в request префикс payload до 1500 байт +для формирования ответа не используется; он не ограничивает длину реального echo payload. -## 3. API +Таймер запускается после первого отправленного запроса, затем каждые 5 с, пока pending +не пуст. Удаляет записи с возрастом **больше** 5 с на очередном обходе; удаление не происходит +точно в момент достижения порога. `destroy` снимает binding, отменяет таймер, закрывает +raw socket/регистрацию, освобождает pending и обнуляет `inst->icmp_proxy`. +Все действия — в uasync; `inst` и `ua` заимствуются до destroy. -### Структуры -| Структура | Назначение | -|-----------|-----------| -| `struct icmp_request` | Состояние активного echo-запроса на exit: client_node_id, dst_ip, orig_src_ip, echo_id, echo_seq, payload, время отправки. Связный список. | -| `struct icmp_proxy_ctx` | Глобальный контекст: is_exit, inst, ua, raw_sock, raw_read_id, список pending, таймаут запросов, флаг test_loopback, таймер очистки. | +## Сообщения и доставка -### Формат пакета -| Смещение | Размер | Поле | -|----------|--------|------| -| 0 | 1 | `svc_id` = `ETCP_RT_ID_ICMP_PROXY` | -| 1 | 1 | `subcmd` = REQUEST (0x01) / REPLY (0x02) | -| 2 | 8 | `sender_node_id` | -| 10 | 4 | `dst_ip` (IP цели для ping) | -| 14 | 4 | `orig_src_ip` (IP исходного отправителя для обратной доставки) | -| 18 | 2 | `echo_id` (icmp_id) | -| 20 | 2 | `echo_seq` (icmp_seq) | -| 22 | N | `payload` (данные ICMP echo) | +Вход `etcp_route_send`: +`svc:1, cmd:1, dst_ip:4, orig_src_ip:4, echo_id:2, echo_seq:2, payload`. +REQUEST=1, REPLY=2, заголовок отправки — 14 байт. В REPLY `dst_ip` остаётся адресом +цели ping, `orig_src_ip` — адресом приложения. Router callback получает +26-байтовые метаданные перед cmd, итого заголовок приёма — 39 байт. +Peer находится в метаданных router; отдельного sender ID в ICMP payload нет. +IP/id/seq передаются в network byte order. -### Функции -| Функция | Назначение | -|---------|-----------| -| `icmp_proxy_init(inst, ua)` | Выделяет контекст, определяет is_exit, на exit создаёт SOCK_RAW и регистрирует его в uasync. Регистрирует коллбэк в etcp_router. | -| `icmp_proxy_destroy(inst)` | Закрывает raw-сокет, отменяет таймер, освобождает pending, отвязывает коллбэк, освобождает память. | -| `icmp_proxy_recv_cb(conn, entry)` | Единый коллбэк etcp_router. По subcmd направляет REQUEST → `exit_handle_request`, REPLY → `client_handle_reply`. | -| `icmp_proxy_send_to_exit(inst, ...)` | Клиент: упаковывает ICMP Echo Request и отправляет через etcp_route_send на exit. | -| `icmp_proxy_deliver_reply(inst, ...)` | Клиент: собирает IP/ICMP Echo Reply-пакет и помещает в TUN input_queue. | -| `icmp_proxy_set_test_loopback(inst, enabled)` | Включает/выключает тестовый loopback-режим на exit (эмуляция echo-ответа без raw-сокета). | +`send_to_exit` копирует payload 0..65507 и возвращает результат router +(0 = принято в очередь, <0 = ошибка). Локальный ICMP-контекст для этой отправки не требуется. +Используются `force=0, mode=0`, собственного повтора REQUEST/REPLY нет. + +Клиент допускает REPLY только от `tcp_proxy_client->via_node_id`. +`deliver_reply(inst, dst_ip, src_ip, ...)` требует клиентский TUN, допускает payload +0..65506, собирает IPv4 `src_ip→dst_ip` с Echo Reply, TTL 64 и обеими checksum. +Нечётный хвост checksum дополняется нулём без чтения за границей. +Результат с префиксом версии 4 ставится в `tun->input_queue`: +0 = поставлен в очередь, -1 = ошибка. Payload заимствован на время вызова. +`recv_cb` принимает владение входным entry/dgram при любом исходе. diff --git a/src/proxy/udp_proxy_doc.md b/src/proxy/udp_proxy_doc.md index 133e19a5..160aab94 100644 --- a/src/proxy/udp_proxy_doc.md +++ b/src/proxy/udp_proxy_doc.md @@ -1,73 +1,48 @@ -# UDP Proxy (udp_proxy) +# UDP proxy -## 1. Назначение -Проксирование UDP-датаграмм между клиентским и exit-узлом по протоколу ETCP. Клиентский узел -принимает UDP-пакеты с TUN-интерфейса, инкапсулирует их и отправляет через `etcp_route_send` -на exit-узел. Exit-узел создаёт нативные UDP-сокеты для каждого потока (5-tuple) и ретранслирует -датаграммы во внешнюю сеть. Ответы проходят обратный путь — от exit к клиенту и затем в TUN. +`udp_proxy.c/h` передаёт UDP между TUN клиента и обычными UDP-сокетами exit через +`ETCP_RT_ID_UDP_PROXY` в `TOPO_GROUP_UTUN`. Общие форматы и ограничения: +[doc/proxy_protocol.md](../../doc/proxy_protocol.md). Контракты API и полей — в +[udp_proxy.h](udp_proxy.h). -Один экземпляр контекста на процесс (глобальный `g_udp_ctx`), режим определяется флагом -`inst->tcp_proxy_server.enabled`: включён → `is_exit=1`, иначе `is_exit=0`. +## Контекст и жизненный цикл -## 2. Как пользоваться +Контекст принадлежит `inst->udp_proxy`, глобального контекста нет. `init(inst, ua)` +создаёт его и запрашивает router binding, повторный init возвращает 0, сохраняя контекст. +`is_exit` — снимок роли при init, но диспетчер проверяет текущее +`inst->tcp_proxy_server.enabled` для REQUEST. REPLY принимается только при наличии +`tcp_proxy_client` и совпадении peer с его `via_node_id`. Один instance может совмещать роли. -### Инициализация -```c -udp_proxy_init(inst, ua); // exit или клиент — определяется автоматически -``` -При инициализации регистрируется коллбэк `udp_proxy_recv_cb` в `etcp_router` для сервиса -`ETCP_RT_ID_UDP_PROXY`. +Exit индексирует flows линейным списком по +`client_node_id + src_ip:src_port + dst_ip:dst_port`. У каждого flow собственный +неблокирующий `AF_INET/SOCK_DGRAM`, привязанный к `INADDR_ANY:0`, и read callback в uasync. +Ответ принимается только от исходного `dst_ip:dst_port`; в REPLY адреса и порты меняются местами. +REQUEST и принятый ответ обновляют `last_activity_tb`. -### Клиент: отправка датаграммы в exit -```c -udp_proxy_send_to_exit(inst, exit_node_id, src_ip, src_port, dst_ip, dst_port, payload, len); -``` -Вызывается из обработчика TUN-трафика. Пакет упаковывается в формат ETCP и отправляется -через `etcp_route_send`. Ответ приходит асинхронно в `udp_proxy_recv_cb`, который вызывает -`udp_proxy_deliver_reply` для записи IP/UDP-пакета обратно в TUN. +Таймер запускается при первом flow и проверяет список каждые 600000 tb (60 с), +пока flows остаются. Удаляет flow при неактивности **больше** 60 с; удаление происходит +на очередном обходе, а не точно в момент достижения порога. `destroy` снимает binding, +отменяет таймер, закрывает сокеты/регистрации и обнуляет `inst->udp_proxy`. +Все операции выполняются в uasync; `inst` и `ua` заимствуются до destroy. -### Exit: приём датаграмм -Exit автоматически принимает REQUEST-ы через `etcp_router`. Для каждого нового потока -(уникальный `client_node_id + src_ip:port + dst_ip:port`) создаётся нативный UDP-сокет, -который регистрируется в `uasync` на чтение. При получении ответа от внешнего хоста -ответ упаковывается и отправляется обратно клиенту. +## Сообщения и доставка -### Очистка -```c -udp_proxy_destroy(inst); // закрывает все flows, отменяет таймер, unbind из etcp_router -``` +На вход `etcp_route_send` передаётся: +`svc:1, cmd:1, src_ip:4, src_port:2, dst_ip:4, dst_port:2, payload`. +REQUEST=1, REPLY=2. Заголовок отправки — 14 байт. Router callback получает 26 байт метаданных +router перед подкомандой, поэтому заголовок приёма — 39 байт. Отдельного `sender_node_id` +в UDP payload нет: peer берётся из метаданных router. Адреса и порты — network byte order. -### Нюансы -- Потоки истекают через 60 секунд неактивности (`UDP_FLOW_TIMEOUT_TB`). Таймер `expire_timer` - запускается при создании первого потока, перезапускается после каждой очистки. -- UDP-сокеты на exit привязываются к `INADDR_ANY:0` (bind), трафик отправляется через `sendto`. -- Ответы в TUN на клиенте доставляются через `inst->tcp_proxy_client->tun->input_queue`. +`send_to_exit` требует созданный UDP-контекст, копирует заимствованный payload +длиной 0..65507 и возвращает результат router: 0 означает принятие в очередь, +отрицательное значение — ошибку. Собственного окна/повтора UDP у модуля нет; +отправка использует `force=0, mode=0`. -## 3. API +`deliver_reply` требует `inst->tcp_proxy_client->tun`, допускает payload 0..65506, +собирает IPv4/UDP с TTL 64, пересчитывает IPv4 checksum и оставляет UDP checksum=0. +Пакет получает однобайтовый префикс версии 4 и ставится в `tun->input_queue`. +Возврат 0 означает постановку в очередь; -1 — отсутствие TUN, недопустимый размер +или ошибку выделения. Входной payload остаётся у вызывающего кода. -### Структуры -| Структура | Назначение | -|-----------|-----------| -| `struct udp_flow` | Состояние одного UDP-потока на exit: 5-tuple, нативный сокет, uasync read_id, времена создания и последней активности. Связный список. | -| `struct udp_proxy_ctx` | Глобальный контекст: флаг is_exit, inst, ua, список flows, таймаут истечения, таймер очистки. | - -### Формат пакета -| Смещение | Размер | Поле | -|----------|--------|------| -| 0 | 1 | `svc_id` = `ETCP_RT_ID_UDP_PROXY` | -| 1 | 1 | `subcmd` = `UDP_PROXY_SUBCMD_DATA` (0x01) | -| 2 | 8 | `sender_node_id` | -| 10 | 4 | `src_ip` (клиент→exit: IP отправителя; exit→клиент: dst_ip) | -| 14 | 2 | `src_port` | -| 16 | 4 | `dst_ip` (клиент→exit: IP назначения; exit→клиент: src_ip) | -| 20 | 2 | `dst_port` | -| 22 | N | `payload` (UDP-датаграмма) | - -### Функции -| Функция | Назначение | -|---------|-----------| -| `udp_proxy_init(inst, ua)` | Выделяет контекст, определяет is_exit, регистрирует коллбэк в etcp_router. Возвращает 0 при успехе. | -| `udp_proxy_destroy(inst)` | Закрывает все потоки, сокеты, отменяет таймеры, отвязывает коллбэк, освобождает память. | -| `udp_proxy_recv_cb(conn, entry)` | Единый коллбэк etcp_router. По subcmd направляет в exit_handle_data (exit) или client_handle_reply (клиент). | -| `udp_proxy_send_to_exit(inst, ...)` | Клиент: упаковывает UDP-датаграмму и отправляет через etcp_route_send на exit. | -| `udp_proxy_deliver_reply(inst, ...)` | Клиент: собирает IP/UDP-пакет и помещает в TUN input_queue для доставки приложению. | +`recv_cb` принимает владение entry/dgram при любом исходе. `init` не пробрасывает +ошибку router bind, поэтому 0 не подтверждает работоспособность регистрации.