14 KiB
ETCP Connections
1. Назначение
Подмодуль ETCP, обслуживающий UDP/TCP-сокеты для передачи кодограмм и управляющий одним ETCP-соединением (ETCP_CONN) через несколько каналов связи (ETCP_LINK, мультилинк с failover).
Отвечает за:
- Создание сокетов из конфига (серверных для входящих и клиентских для исходящих)
- Приём сырых пакетов, дешифровку, диспетчеризацию (INIT/PING/PONG/KEEPALIVE/данные)
- INIT handshake: обмен node_id, ключами pubkey (X25519 + Ed25519), MTU, keepalive-параметрами
- Keepalive с адаптивным периодом (200мс → 10с → 200мс при трафике)
- Управление inflight-лимитами, burst-измерения bandwidth, BBR congestion control per-link
- Детекцию NAT (DIRECT/EIM/strict), обновление nat_addr при смене адреса
2. Как пользоваться
Инициализация сокетов и соединений
Двухфазная инициализация:
// Фаза 1: создаём listen-сокеты (до topo_group_init)
init_sockets(instance);
// Фаза 2: создаём клиентские соединения из [client] секций конфига
init_connections(instance);
Обе функции берут конфиг из instance->config: серверы из [server], клиенты из [client].
Входящий пакет: полный путь
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-стек]
│
└─ Нет линка/сессии → попытка INIT-дешифровки
├─ Извлечь соль + obfuscated pubkey из хвоста пакета
├─ sc_obfuscate_pubkey() → восстановить pubkey пира
├─ sc_set_peer_public_key() → вычислить session_key
├─ sc_decrypt() → расшифровать
├─ PING → handle_ping(): ответить PONG, вернуть данные коллбэку
├─ PONG → handle_pong(): найти PING_CONTEXT, вызвать cb
└─ INIT_REQUEST → обработать handshake, отправить INIT_RESPONSE
Отправка зашифрованного пакета
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);
etcp_encrypt_send делает:
sc_encrypt(timestamp(2) + flag_up(1) + data, ...)→ зашифрованный буфер- Копирует
noencrypt_lenбайт из хвоста данных в конец буфера (для pubkey при INIT) etcp_udp_send()→socket_sendto()(илиlink->send_hookесли установлен)
PING/PONG (one-shot пробники)
etcp_send_ping(instance, peer_pubkey_bin, &addr, timeout_ms,
my_ping_callback, user_arg,
user_data, user_data_len);
// Коллбэк:
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 обфусцируется в хвосте.
Адаптивный keepalive
- Период
ka_period_ms: старт =keepalive_intervalиз конфига, минимальный 200мс - Если был трафик (
pkt_sent_since_keepalive=1): период сбрасывается кkeepalive_interval - Без трафика: период растёт ×1.05 до
KA_PERIOD_MAX_MS(10с) - Таймаут =
period × KA_TIMEOUT_MULT(×10). При превышении → линк падает - Сервер не шлёт 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]=режим.
Режимы:
KA_MODE_NORMAL(0) — активен: интервалKA_MOBILE_ACTIVE_MS(3с), адаптивный (рост 3с → 10с, сброс на трафике).KA_MODE_STANDBY(1) — спит: интервалKA_MOBILE_STANDBY_MS(30с), период зафиксирован (ka_pinned=1), адаптивный рост отключён.
Согласование:
- Мобильный отвечающий считает
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).
3. API
Ключевые структуры
ETCP_SOCKET — один слушающий/клиентский сокет из конфига:
fd— UDP (или TCP черезstcp_link)links[]— сортированный массив активных линков (поip_port_hash), бинарный поискinterface_addr— IP интерфейса (автоопределённый или из конфига)nat_addr,nat_type— детектированный NAT-адрес и типtype—CFG_SERVER_TYPE_PUBLIC/NAT/PRIVATEsock_id— уникальный ID (0-255) для сопоставления при NAT-матчинге
ETCP_LINK — одно динамическое соединение (один путь между двумя узлами):
- Принадлежит ровно одному
ETCP_CONN(родитель) и одномуETCP_SOCKET(сокет) remote_addr— адрес пира; поиск поip_port_hash(CRC32 от sockaddr)mtu,mtu_local,mtu_remote— согласованный MTU (min)is_server— 0=клиент инициирует, 1=сервер принимаетinitialized=1— handshake завершён, соединение готовоlink_status— итоговый статус:recv_keepalive && remote_keepalivelink_state: 0=init, 1=handshake, 2=reconnect, 3=connectedinit_timer/keepalive_timer/shaper_timer— таймеры uasyncremote_ed25519_pubkey— Ed25519-ключ пира из INIT handshakebbr— состояние BBR congestion control per-link- Поля burst-измерений bandwidth (sender + receiver)
send_hook— перехватчик отправки (для net_emulator)- Статистика: encrypt/decrypt/send/recv errors, total_encrypted/decrypted, retransmissions
ETCP_DGRAM — незашифрованная кодограмма:
link— куда отправлять / откуда полученоdata_len— общий размер данных (без timestamp)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]link_id,socket_id,only_local,typesrc_ipv4[4],src_port[2]— для NAT_DIRECT-детекцииcollision— флаг разрешения коллизийed25519_pubkey[32]
ETCP_INIT_RESPONSE_PKT (57 байт) — ответ сервера:
code(0x03/0x05),node_id[8],session_id[4],mtu[2]link_id,remote_socket_id,only_local,typepeer_ipv4[4],peer_port[2]— внешний NAT-адрес клиентаed25519_pubkey[32]
Типы кодограмм
| Константа | Значение | Назначение |
|---|---|---|
ETCP_INIT_REQUEST |
0x02 | INIT-запрос со сбросом ETCP-соединения |
ETCP_INIT_RESPONSE |
0x03 | INIT-ответ со сбросом |
ETCP_INIT_REQUEST_NOINIT |
0x04 | INIT-запрос без сброса |
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) |
Основные функции
| Функция | Назначение |
|---|---|
init_sockets(inst) |
Создать listen-сокеты из [server] конфига (до topo_group_init) |
init_connections(inst) |
Создать клиентские соединения из [client] + сокеты если не созданы |
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) |
Обновить лимит inflight (clamped к [8K, 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_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_remote_id(conn, remote_link_id) |
Найти линк по ID пира |
etcp_find_free_local_link_id(etcp) |
Найти свободный local_link_id (0-255, битовая карта) |
etcp_link_enter_init(link) |
Начать INIT handshake (link_state=1) |
etcp_link_enter_reinit(link) |
Начать переподключение (link_state=2, без сброса ETCP) |
Burst-измерения
| Функция | Назначение |
|---|---|
etcp_link_burst_start(link) |
Начать burst-замер bandwidth |
etcp_link_burst_check(link) |
Проверить условия для старта burst |
etcp_link_burst_finish(link) |
Завершить burst, установить таймер ожидания ответа |
Константы
| Константа | Значение | Назначение |
|---|---|---|
INFLIGHT_LIM_MIN |
8192 | Минимальный inflight (8K) |
INFLIGHT_LIM_MAX |
1048576 | Максимальный inflight (1M) |
PACKET_DATA_SIZE |
1600 | Размер буфера пакета |
PACKET_DATA_MAX_MTU |
1600 | Максимальный MTU |
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 |