diff --git a/src/transport_layer/etcp_connections_doc.md b/src/transport_layer/etcp_connections_doc.md index 666259bd..a988ee54 100644 --- a/src/transport_layer/etcp_connections_doc.md +++ b/src/transport_layer/etcp_connections_doc.md @@ -2,15 +2,17 @@ ## 1. Назначение -Подмодуль ETCP, обслуживающий UDP/TCP-сокеты для передачи кодограмм и управляющий одним ETCP-соединением (`ETCP_CONN`) через несколько каналов связи (`ETCP_LINK`, мультилинк с failover). +Подмодуль обслуживает UDP/TCP-сокеты и физические пути (`ETCP_LINK`) общего +соединения с пиром (`ETCP_CONN`). Сокет обслуживает несколько пиров, одно соединение +может использовать несколько UDP/STCP-линков. Все вызовы выполняются в uasync-потоке instance. Отвечает за: -- Создание сокетов из конфига (серверных для входящих и клиентских для исходящих) +- Создание серверных сокетов из конфига и подключение линков, которыми управляет NCD - Приём сырых пакетов, дешифровку, диспетчеризацию (INIT/PING/PONG/KEEPALIVE/данные) - INIT handshake: обмен node_id, ключами pubkey (X25519 + Ed25519), MTU, keepalive-параметрами -- Keepalive с адаптивным периодом (200мс → 10с → 200мс при трафике) +- Keepalive, отдельный RX watchdog и согласование сна через флаги keepalive-пакета - Управление inflight-лимитами, burst-измерения bandwidth, BBR congestion control per-link -- Детекцию NAT (DIRECT/EIM/strict), обновление nat_addr при смене адреса +- Обмен наблюдаемыми endpoint; профили адресов и проверки NAT ведут socket_monitor/nat_detection ## 2. Как пользоваться @@ -18,14 +20,17 @@ Двухфазная инициализация: ```c -// Фаза 1: создаём listen-сокеты (до topo_group_init) -init_sockets(instance); +// Ядро создаёт listen-сокеты до инициализации групп. +if (init_sockets(instance) < 0) return -1; -// Фаза 2: создаём клиентские соединения из [client] секций конфига -init_connections(instance); +// Подготовить сокеты, NCD control и подписки keepalive. +if (init_connections(instance) < 0) return -1; ``` -Обе функции берут конфиг из `instance->config`: серверы из `[server]`, клиенты из `[client]`. +`init_connections` использует результат прежнего `init_sockets` либо вызывает его, +если сокетов ещё нет. При `auto_sockets` сокеты создаёт отдельный модуль. +Запросы к `[client]` запускает UTUN-сервис через `init_client_connections`. +Другие владельцы открывают транспорт через `node_conn_direct_open/open_node` и ждут `NCD_EVENT_UP`. ### Входящий пакет: полный путь @@ -34,14 +39,15 @@ socket_recvfrom() → etcp_connections_read_callback_socket() ├─ Есть линк и session_ready → sc_decrypt() по session_key │ ├─ KEEPALIVE → обновить period, выйти │ ├─ INIT_RESPONSE → handle_init_response_client() - │ └─ Данные → etcp_conn_input(pkt) [в ETCP-стек] + │ ├─ HELLO/CHALLENGE/CONFIRM → etcp_session + │ └─ SESSION_DATA → проверить эпохи, снять обёртку → etcp_conn_input(pkt) │ - └─ Нет линка/сессии → попытка INIT-дешифровки + └─ Нет ключей линка или обычная дешифровка не удалась → попытка INIT-дешифровки ├─ Извлечь соль + obfuscated pubkey из хвоста пакета ├─ sc_obfuscate_pubkey() → восстановить pubkey пира ├─ sc_set_peer_public_key() → вычислить session_key ├─ sc_decrypt() → расшифровать - ├─ PING → handle_ping(): ответить PONG, вернуть данные коллбэку + ├─ PING → handle_ping(): обработать пробу, обычно ответить PONG ├─ PONG → handle_pong(): найти PING_CONTEXT, вызвать cb └─ INIT_REQUEST → обработать handshake, отправить INIT_RESPONSE ``` @@ -49,19 +55,19 @@ socket_recvfrom() → etcp_connections_read_callback_socket() ### Отправка зашифрованного пакета ```c -struct ETCP_DGRAM* dgram = memory_pool_alloc(inst->pkt_pool); -dgram->link = link; -dgram->data[0] = ETCP_KEEPALIVE; -dgram->data_len = 1; -dgram->noencrypt_len = 0; // для INIT — SC_PUBKEY_ENC_SIZE -etcp_encrypt_send(dgram); -memory_pool_free(inst->pkt_pool, dgram); +#include "etcp_keepalive.h" + +// Сформировать корректный keepalive с периодом, flags и padding и сразу отправить. +etcp_link_send_keepalive(link); ``` `etcp_encrypt_send` делает: -1. `sc_encrypt(timestamp(2) + flag_up(1) + data, ...)` → зашифрованный буфер -2. Копирует `noencrypt_len` байт из хвоста данных в конец буфера (для pubkey при INIT) -3. `etcp_udp_send()` → `socket_sendto()` (или `link->send_hook` если установлен) +1. Заполняет timestamp(2) и flag_up(1); перед секциями DATA/ACK добавляет 17-байтную обёртку с эпохами сессии. +2. Для UDP шифрует тело AES-128-CCM и добавляет открытый хвост `noencrypt_len` (40 байт salt/pubkey у INIT-запроса). +3. Отправляет UDP через SOCKS5-релей сокета, иначе `send_hook`, иначе `socket_sendto`. +4. Для TCP передаёт сформированное тело в `stcp_link_send`; шифрование и очередь обеспечивает STCP/REALITY. + +`etcp_encrypt_send` не освобождает переданный `ETCP_DGRAM`; отрицательный результат означает ошибку отправки. ### PING/PONG (one-shot пробники) @@ -75,61 +81,61 @@ void my_ping_callback(int success, uint16_t rtt, void* arg, uint64_t nonce, const uint8_t* resp_data, size_t resp_data_len); ``` -Пинг-пакеты идут мимо ETCP_CONN/LINK — через временный `sc_context_t`, pubkey обфусцируется в хвосте. +PING/PONG не требуют ETCP_CONN/LINK: используются временный `sc_context_t` и +обфусцированный pubkey в хвосте. RTT callback измерен в 0.1 мс; данные ответа +заимствованы только на время callback. `etcp_cancel_pings` отменяет совпадающие +callback/arg без уведомления; аргумент освобождает владелец запроса. ### Адаптивный keepalive -- Период `ka_period_ms`: старт = `keepalive_interval` из конфига, минимальный 200мс +- Базовый `keepalive_interval` согласуется по типам устройств: при наличии mobile выбирается max, иначе min двух значений +- Период `ka_period_ms` начинается с базового интервала; UDP handshake ограничивает базу снизу 10 мс - Если был трафик (`pkt_sent_since_keepalive=1`): период сбрасывается к `keepalive_interval` -- Без трафика: период растёт ×1.05 до `KA_PERIOD_MAX_MS` (10с) -- Таймаут = `period × KA_TIMEOUT_MULT` (×10). При превышении → линк падает +- Без трафика и при `keepalive_adaptive` период растёт как `period * 105 / 100 + 1`, максимум 10 с +- Принятый ненулевой период пира задаёт `keepalive_timeout = period * KA_TIMEOUT_MULT` (×10) +- Отдельный RX watchdog отсчитывает срок от последнего допустимого приёма; собственные отправки не продлевают жизнь линка - Сервер не шлёт keepalive при `recv_keepalive=0` (линк уже мёртв), клиент шлёт всегда -### Протокол смены keepalive-режима (standby/normal, мобильные узлы) +### Сон и пробуждение -Модуль `etcp_keepalive.c/h`. Мобильный узел (Android) при смене active mode -(передний/фоновый план) сообщает пиру желаемый keepalive-режим через кодограмму -`ETCP_KEEPALIVE_REQ` (0x09); пир применяет согласованный режим и отвечает -`ETCP_KEEPALIVE_RESP` (0x0A). Формат: `data[0]=code`, `data[1]=режим`. +Сон передаётся тем же `ETCP_KEEPALIVE` (0x08). Обязательное тело: +`code(1) + flags(1) + period_ms(2 LE)`; при `KA_FLAG_SLEEP` добавляются +`active_remaining(2 LE) + sleep_duration(2 LE)` в единицах 0.1 с. Далее допустим нулевой padding до `ETCP_PADDING_MAX`. -Режимы: -- `KA_MODE_NORMAL` (0) — активен: интервал `KA_MOBILE_ACTIVE_MS` (3с), адаптивный - (рост 3с → 10с, сброс на трафике). -- `KA_MODE_STANDBY` (1) — спит: интервал `KA_MOBILE_STANDBY_MS` (30с), период - зафиксирован (`ka_pinned=1`), адаптивный рост отключён. +При сне одной из сторон обычный периодический keepalive останавливается. +Спящий узел объявляет фазу `KA_FLAG_SLEEP`, UDP-пир отвечает `KA_FLAG_SLEEP_ACK`; +у TCP отдельное подтверждение не требуется. UDP-анонс повторяется до ACK или +исчерпания попыток; накопленные провалы приводят к link DOWN. +Обычный keepalive сообщает пробуждение, снимает фазовый таймер пира и возвращает normal-режим. -Согласование: -- Мобильный отвечающий считает `agreed = standby`, если его или запрошенный режим - standby (эквивалент `max` интервалов), иначе `normal`. -- Не-мобильный отвечающий применяет запрошенный режим как есть. -- Инициатор применяет согласованный режим только после получения RESP. -- Применение: `keepalive_interval = ka_period_ms = ms`, `keepalive_timeout = ms * KA_TIMEOUT_MULT`, - `ka_pinned` по режиму, рестарт keepalive-таймера. - -Смена active mode рассылается через событие `utun_add_activity_cbk` -(подписчики: `topo_group_on_activity_change`, `etcp_keepalive_on_activity`). +RX watchdog действует и во сне. К обычному timeout применяется максимум с +`(active_remaining + sleep_duration) * 100мс + keepalive_interval * 10`. +Фазы и повторные отправки сами по себе не продлевают срок от последнего RX. +С `UTUN_HAVE_STANDBY` модуль подписан на switch/phase callbacks standby; +в остальных сборках — на активность мобильного клиента. Формат и детали: `etcp_keepalive.h`. ## 3. API ### Ключевые структуры **ETCP_SOCKET** — один слушающий/клиентский сокет из конфига: -- `fd` — UDP (или TCP через `stcp_link`) -- `links[]` — сортированный массив активных линков (по `ip_port_hash`), бинарный поиск +- `fd` — UDP-сокет или TCP listener +- `links_queue` — хеш-очередь UDP-линков с 19-байтным ключом port/addr/family - `interface_addr` — IP интерфейса (автоопределённый или из конфига) -- `nat_addr`, `nat_type` — детектированный NAT-адрес и тип -- `type` — `CFG_SERVER_TYPE_PUBLIC/NAT/PRIVATE` +- `nat_addr`, `address_profile` — endpoint NAT и профиль mapping/inbound/проверок адреса +- `type` — конфигурационный `CFG_SERVER_TYPE_*` - `sock_id` — уникальный ID (0-255) для сопоставления при NAT-матчинге **ETCP_LINK** — одно динамическое соединение (один путь между двумя узлами): -- Принадлежит ровно одному `ETCP_CONN` (родитель) и одному `ETCP_SOCKET` (сокет) -- `remote_addr` — адрес пира; поиск по `ip_port_hash` (CRC32 от sockaddr) +- Принадлежит одному `ETCP_CONN`; TCP-клиент может работать без локального `ETCP_SOCKET` +- `remote_addr` — адрес пира; для UDP поиск в `links_queue` соответствующего сокета - `mtu`, `mtu_local`, `mtu_remote` — согласованный MTU (min) - `is_server` — 0=клиент инициирует, 1=сервер принимает -- `initialized=1` — handshake завершён, соединение готово +- `initialized=1` — handshake линка принят; готовность потока также зависит от подтверждения сессии - `link_status` — итоговый статус: `recv_keepalive && remote_keepalive` - `link_state`: 0=init, 1=handshake, 2=reconnect, 3=connected - `init_timer` / `keepalive_timer` / `shaper_timer` — таймеры uasync +- `rx_timeout_timer` — независимый контроль приёма; `retrans_timer` — таймер физических DATA-попыток этого пути - `bbr` — состояние BBR congestion control per-link - Поля burst-измерений bandwidth (sender + receiver) - `send_hook` — перехватчик отправки (для net_emulator) @@ -137,65 +143,68 @@ void my_ping_callback(int success, uint16_t rtt, void* arg, uint64_t nonce, **ETCP_DGRAM** — незашифрованная кодограмма: - `link` — куда отправлять / откуда получено -- `data_len` — общий размер данных (без timestamp) +- `data_len` — длина data, включая открытый хвост, без timestamp/flag_up - `noencrypt_len` — байты с конца, не подлежащие шифрованию (SC_PUBKEY_ENC_SIZE для INIT) - `timestamp`, `flag_up` — заголовок, шифруется вместе с data -**ETCP_INIT_REQUEST_PKT** (62 байта) — формат INIT-запроса в зашифрованном payload: -- `code` (0x02/0x04), `node_id[8]`, `session_id[4]`, `mtu[2]`, `keepalive[2]`, `recovery[2]` +**ETCP_INIT_REQUEST_PKT** (67 байт) — обязательное тело запроса, до padding: +- `code` (0x02/0x04), `node_id[8]`, `reset_id[8]`, `mtu[2]`, `keepalive[2]`, `recovery[2]` - `link_id`, `socket_id`, `only_local`, `type` -- `src_ipv4[4]`, `src_port[2]` — для NAT_DIRECT-детекции +- `src_ipv4[4]`, `src_port[2]` — заявленный локальный endpoint клиента - `collision` — флаг разрешения коллизий -- `ed25519_pubkey[32]` +- `ed25519_pubkey[32]`, `device_type` -**ETCP_INIT_RESPONSE_PKT** (57 байт) — ответ сервера: -- `code` (0x03/0x05), `node_id[8]`, `session_id[4]`, `mtu[2]` +**ETCP_INIT_RESPONSE_PKT** (64 байта) — обязательное тело ответа, до padding: +- `code` (0x03/0x05), `node_id[8]`, `reset_id[8]`, `mtu[2]` - `link_id`, `remote_socket_id`, `only_local`, `type` - `peer_ipv4[4]`, `peer_port[2]` — внешний NAT-адрес клиента -- `ed25519_pubkey[32]` +- `ed25519_pubkey[32]`, `device_type`, `keepalive[2]` + +Числовые поля INIT длиной более байта записываются BE. Каждая сторона сообщает собственный `reset_id`. +Повтор INIT не сбрасывает надёжный поток: смену известной эпохи подтверждает HELLO/CHALLENGE/CONFIRM. +До изменения состояния проверяются размер тела, node_id по X25519-ключу, ненулевой link_id и минимальный UDP MTU. +Минимум равен `ETCP_MIN_UDP_MTU(family)` (191 байт IPv4, 211 IPv6); MTU больше 1500 ограничивается сверху. + +Известный пир входящего UDP/TCP индексируется в реестре до callback создания линка. +Публикация `INIT/UP` происходит отдельно. Callback может отменить линк, открыть NCD handle или создать другой путь; +отмена входящего линка сохраняет новый conn при наличии другого линка или NCD-владельца. +`etcp_link_close` сразу отключает линк, снимает таймеры и выполняет уведомления; память удерживается `active_users` +до выхода последних внутренних стеков конструктора/RX/callback. Подробный контракт — в `etcp_connections.h`. ### Типы кодограмм | Константа | Значение | Назначение | |-----------|----------|-----------| -| `ETCP_INIT_REQUEST` | 0x02 | INIT-запрос со сбросом ETCP-соединения | -| `ETCP_INIT_RESPONSE` | 0x03 | INIT-ответ со сбросом | -| `ETCP_INIT_REQUEST_NOINIT` | 0x04 | INIT-запрос без сброса | -| `ETCP_INIT_RESPONSE_NOINIT` | 0x05 | INIT-ответ без сброса | +| `ETCP_INIT_REQUEST` | 0x02 | Начальный handshake UDP-линка | +| `ETCP_INIT_RESPONSE` | 0x03 | Вариант INIT-ответа | +| `ETCP_INIT_REQUEST_NOINIT` | 0x04 | Восстановление UDP-линка / разрешение коллизии | +| `ETCP_INIT_RESPONSE_NOINIT` | 0x05 | Второй вариант INIT-ответа; эпоха проверяется так же | | `ETCP_PING` | 0x06 | One-shot пробник | | `ETCP_PONG` | 0x07 | Ответ на пробник | | `ETCP_KEEPALIVE` | 0x08 | keepalive-пакет | -| `ETCP_KEEPALIVE_REQ` | 0x09 | Запрос смены keepalive-режима (standby/normal) | -| `ETCP_KEEPALIVE_RESP` | 0x0A | Ответ: согласованный режим | - -### NAT-типы - -| Константа | Значение | Назначение | -|-----------|----------|-----------| -| `NAT_TYPE_UNKNOWN` | 0 | Не определён | -| `NAT_TYPE_EIM` | 1 | Endpoint-Independent Mapping | -| `NAT_TYPE_STRICT` | 2 | Address/Restricted или Symmetric | -| `NAT_TYPE_DIRECT` | 3 | Реальный публичный IP (нет NAT) | -| `NAT_VERIFIED_*` (4-7) | — | Верифицированные (опубликованные в nodeinfo) | +| `ETCP_SESSION_DATA` | 0x09 | Секции DATA/ACK с эпохами отправителя и получателя | +| `ETCP_SESSION_HELLO/CHALLENGE/CONFIRM` | 0x0A/0x0B/0x0C | Подтверждение сессии | +| `ETCP_TCP_ADDRESS` | 0x0D | STCP: наблюдаемый IP клиента, без порта | ### Основные функции | Функция | Назначение | |---------|-----------| | `init_sockets(inst)` | Создать listen-сокеты из `[server]` конфига (до `topo_group_init`) | -| `init_connections(inst)` | Создать клиентские соединения из `[client]` + сокеты если не созданы | +| `init_connections(inst)` | Подготовить listen-сокеты, NCD control и keepalive; 0/-1 | +| `init_client_connections(inst, clients)` | Заменить запросы UTUN-группы к пирам конфигурации; 0/-1 | | `etcp_socket_add(inst, srv)` | Создать UDP-сокет из `CFG_SERVER`: bind, non-block, send/recv buffers 4MB | | `etcp_socket_remove(e_sock)` | Закрыть сокет, удалить все линки, освободить память | | `etcp_link_new(etcp, conn, addr, is_server)` | Создать ETCP_LINK: выделить local_link_id, BBR, вставить в сокет и ETCP_CONN | | `etcp_link_close(link)` | Удалить линк: таймеры, удаление из списков сокета и ETCP_CONN | | `etcp_link_update_inflight_lim(link, new_lim)` | Применить положительное окно DATA, ограничив сверху max_inflight; ноль отклоняется | -| `etcp_encrypt_send(dgram)` | Зашифровать dgram (SC_CCM) + UDP-отправка через `etcp_udp_send` | -| `etcp_udp_send(link, fd, buf, len, addr, len)` | Отправка: если `send_hook` — через него, иначе `socket_sendto` | +| `etcp_encrypt_send(dgram)` | Сформировать защищённый UDP-пакет либо передать тело в STCP; dgram остаётся вызывающему | +| `etcp_udp_send(link, fd, buf, len, addr, addr_len)` | SOCKS5 сокета, иначе send_hook, иначе socket_sendto | | `etcp_send_ping(inst, pubkey, addr, timeout, cb, arg, data, len)` | One-shot PING | | `etcp_send_ping_to_socket(inst, sock, pubkey, addr, ...)` | PING через конкретный сокет | -| `etcp_link_find_by_addr(e_sock, addr)` | Найти линк по адресу (бинарный поиск в `links[]`) | +| `etcp_link_find_by_addr(e_sock, addr, is_tcp)` | Хеш-поиск в links_queue сокета; is_tcp участвует в ключе; без очереди возвращает NULL | | `etcp_link_find_by_remote_id(conn, remote_link_id)` | Найти линк по ID пира | -| `etcp_find_free_local_link_id(etcp)` | Найти свободный local_link_id (0-255, битовая карта) | +| `etcp_find_free_local_link_id(etcp)` | Свободный local_link_id в 1..255 циклически после последнего назначенного; -1 при исчерпании | | `etcp_link_enter_init(link)` | Начать INIT handshake (link_state=1) | | `etcp_link_enter_reinit(link)` | Начать переподключение (link_state=2, без сброса ETCP) | @@ -215,9 +224,8 @@ void my_ping_callback(int success, uint16_t rtt, void* arg, uint64_t nonce, | `INFLIGHT_LIM_MAX` | 1048576 | Максимальный inflight (1M) | | `PACKET_DATA_SIZE` | 2048 | Размер внутреннего буфера пакета | | `PACKET_DATA_MAX_MTU` | 1500 | Максимальный wire-MTU UDP | -| `KA_PERIOD_MIN_MS` | 200 | Мин. период keepalive | | `KA_PERIOD_MAX_MS` | 10000 | Макс. период keepalive | | `KA_TIMEOUT_MULT` | 10 | Множитель таймаута = period×10 | | `ACK_REZERV` | 100 | Резерв байт под ACK | -| `INIT_TIMEOUT_INITIAL` | 500ms | Начальный таймаут INIT | -| `INIT_TIMEOUT_MAX` | 50000ms | Максимальный таймаут INIT | +| `INIT_TIMEOUT_INITIAL` | 500 TB = 50 мс | Начальный интервал повторов INIT | +| `INIT_TIMEOUT_MAX` | 50000 TB = 5 с | Максимальный интервал повторов INIT | diff --git a/src/transport_layer/secure_channel.h b/src/transport_layer/secure_channel.h index fe26c203..4e2dd81c 100644 --- a/src/transport_layer/secure_channel.h +++ b/src/transport_layer/secure_channel.h @@ -26,7 +26,7 @@ extern "C" { #define SC_TAG_SIZE 16 #define SC_CRC32_SIZE 4 -// Обфускация pubkey при передаче (salt + double SHA256 XOR) +// Обфускация pubkey при передаче: salt(8) + XOR ключа с SHA256(salt || peer_pubkey). #define SC_PUBKEY_ENC_SALT_SIZE 8 #define SC_PUBKEY_ENC_SIZE (SC_PUBKEY_SIZE + SC_PUBKEY_ENC_SALT_SIZE) @@ -81,7 +81,9 @@ sc_status_t sc_decrypt(sc_context_t *ctx, const uint8_t *ciphertext, size_t ciph // SHA256-based transcode: XOR data with SHA256(key) sc_status_t sc_sha_transcode(const uint8_t *key, size_t key_len, uint8_t *data, size_t data_len); -// Obfuscate pubkey при передаче: XOR с SHA256(salt+peer_pubkey) || SHA256(peer_pubkey+salt) +/* output[32] = pubkey[32] XOR SHA256(salt[8] || peer_pubkey[32]); все аргументы обязательны, бинарные. + * Для снятия маски peer_pubkey — собственный ключ получателя, pubkey — принятый обфусцированный ключ. + * SC_OK/SC_ERR_INVALID_ARG. Второй вычисляемый в реализации SHA256 не участвует в 32-байтной маске. */ sc_status_t sc_obfuscate_pubkey(const uint8_t *salt, const uint8_t *peer_pubkey, const uint8_t *pubkey, uint8_t *output); // --- Streaming cipher (AES-128-CTR, confidentiality only) ---