Align connection documentation with current UDP sessions and keepalive wire format

This commit is contained in:
evgeny committed 2026-10-24 00:27:14 +02:00
1 parent 1cf2cc10dd
commit ecdd767934
2 files changed
+97 -87

No files matched your search

+93 -85
View File
@@ -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 |
+4 -2
View File
@@ -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) ---