docs: align ETCP handshake and epoch descriptions with code

This commit is contained in:
evgeny committed 2026-10-21 15:37:01 +02:00
1 parent f59f64ea48
commit d644126cac
3 files changed
+81 -47

No files matched your search

+67 -38
View File
@@ -65,8 +65,12 @@ Burst-измерения bandwidth (0x07, 0x08) зарезервированы,
шифруется строго ВСЁ включая ВСЕ заголовки (кроме обфусцированного publickey в INIT-пакетах).
timestamp вставляется ВСЕГДА ВО ВСЕ КОДОГРАММЫ (скрыто в шифрованном заголовке).
Итого шифруется: 3 байта заголовка + (data_len - noencrypt_len) байт данных.
2. Опциональные секции (одна или несколько) добавляются в etcp.c (внутри data[], без доп. заголовка):
Для DATA/ACK после этих 3 байт добавляется обязательный заголовок сессии:
[0x09][sender_epoch(8 BE)][receiver_epoch(8 BE)], затем идут секции из etcp.c.
Итого шифруется: 3 + 17 + data_len байт для DATA/ACK;
для остальных кодограмм — 3 + (data_len - noencrypt_len) байт.
Формат и подтверждение эпох описаны в разделе «Подтверждение транспортной сессии» ниже.
2. Опциональные секции (одна или несколько) добавляются в etcp.c внутри DATA/ACK после заголовка сессии:
а) Подтверждения (ACK) - заголовок 0x01:
[0x01][count][last_delivered_id(4 байта LE)][rx_dup_count(2 байта LE)][[seq(4 LE)][recv_ts(2 LE)][dly(2 LE)] × count]
- count: количество ACK записей (1 байт; до 32 ограничено размером пакета)
@@ -114,56 +118,80 @@ Burst-измерения bandwidth (0x07, 0x08) зарезервированы,
- клиент отправляет init запрос и выставляет таймаут
- по таймауту повторяет init запросы с экспоненциально растущим интервалом (начальный 500ms, макс 50s, +25% каждые 10 попыток), ведется счетчик запросов init.
- когда клиент получает init подтверждение — снимает таймаут, выставляет link_state=connected, запускает keepalive.
- сервер при получении init запроса: если линк существует и session_id совпал — отвечает без сброса (0x05); если session_id новый — выполняет etcp_conn_reinit и отвечает со сбросом (0x03).
При добавлении нового канала: если link_state=1 (handshake) — посылаем INIT со сбросом (0x02); если link_state=2 (reconnect) — без сброса (0x04).
- каждая сторона сообщает в INIT собственный reset_id (64 бита, BE); ответ не повторяет эпоху запроса.
- первая неизвестная эпоха пира сохраняется из handshake. Смена уже известной эпохи запускает challenge;
только подходящий CONFIRM сбрасывает поток и заменяет эпоху пира, сохраняя собственную.
- повтор INIT и коллизия встречных подключений сами по себе не сбрасывают поток.
При добавлении нового канала: если link_state=1 (handshake) — посылаем INIT_REQUEST (0x02);
если link_state=2 (reconnect) — INIT_REQUEST_NOINIT (0x04). Коды INIT не заменяют подтверждение смены эпохи.
link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
**** Формат кодограмм для etcp_connections.c/h ****
Кодограммы с этими секциями обрабатываются в etcp_connections (в этих кодограммах всегда одна секция-тип пакета). В обязательном заголовке (timestamp+flag_up) ID не используется.
Типы пакетов:
0x02 — ETCP_INIT_REQUEST (со сбросом etcp сессии)
0x03 — ETCP_INIT_RESPONSE (подтверждение со сбросом)
0x04 — ETCP_INIT_REQUEST_NOINIT (без сброса)
0x05 — ETCP_INIT_RESPONSE_NOINIT (подтверждение без сброса)
0x02 — ETCP_INIT_REQUEST (начальный handshake линка)
0x03 — ETCP_INIT_RESPONSE (вариант ответа INIT)
0x04 — ETCP_INIT_REQUEST_NOINIT (восстановление линка, также разрешение коллизии)
0x05 — ETCP_INIT_RESPONSE_NOINIT (вариант ответа INIT)
0x06 — ETCP_PING (проверка доступности)
0x07 — ETCP_PONG (ответ на пинг)
0x08 — ETCP_KEEPALIVE
0x09 — ETCP_SESSION_DATA (обёртка секций DATA/ACK с эпохами)
0x0a — ETCP_SESSION_HELLO
0x0b — ETCP_SESSION_CHALLENGE
0x0c — ETCP_SESSION_CONFIRM
0x0d — ETCP_TCP_ADDRESS (STCP: code + family(4/6) + observed IP(16), без порта)
Варианты ответа 0x03/0x05 не определяют смену эпохи: клиент в обоих случаях обрабатывает reset_id одинаково.
1) Init запрос (0x02 / 0x04):
[code(1)] [my_node_id(8 BE)] [session_id(4 BE)] [mtu_local(2)] [keepalive_interval(2)]
[recovery_interval/100(2)] [local_link_id(1)] [sock_id(1)] [only_local(1)] [type(1)]
[code(1)] [node_id(8 BE)] [reset_id(8 BE)] [mtu(2 BE)] [keepalive(2 BE)]
[recovery(2 BE)] [link_id(1)] [socket_id(1)] [only_local(1)] [type(1)]
[src_ipv4(4)] [src_port(2 BE)] [collision(1)] [ed25519_pubkey(32)] [device_type(1)]
[random_padding...] [salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
- code: 0x02 (со сбросом) или 0x04 (без сброса)
Фиксированное тело до padding — 67 байт (ETCP_INIT_REQUEST_PKT).
- code: 0x02 или 0x04, вариант handshake линка
- node_id: 64 бита, идентификатор узла (big-endian)
- session_id: 32 бита, случайный ID сессии генерируется клиентом (big-endian)
- mtu_local: uint16_t локальный MTU
- keepalive_interval: uint16_t интервал keepalive в мс
- recovery_interval: uint16_t = recovery_interval/100 (в таймбазах 0.1мс / 100)
- local_link_id: uint8_t (0-255), назначается отправителем
- sock_id: uint8_t, уникальный ID сокета отправителя
- reset_id: uint64_t, собственная локальная эпоха потока клиента (big-endian)
- mtu: uint16_t локальный MTU
- keepalive: uint16_t интервал keepalive в мс
- recovery: uint16_t = link->recovery_interval/100 (исходная единица — 0.1 мс)
- link_id: uint8_t (0-255), назначается отправителем
- socket_id: uint8_t, уникальный ID сокета отправителя
- only_local: uint8_t, флаг "только локальные подключения"
- type: uint8_t, тип сокета (CFG_SERVER_TYPE_UNKNOWN=0/PUBLIC=1/NAT=2/PRIVATE=3/LOCAL=4)
- type: uint8_t, тип сокета (CFG_SERVER_TYPE_UNKNOWN=0/PUBLIC=1/NAT=2/PRIVATE=3/LOCAL=4)
- src_ipv4/src_port: адрес интерфейса отправителя; для IPv6 src_ipv4=0, порт сохраняется;
при SOCKS5 оба поля нулевые. Это заявленный адрес, а не наблюдение внешнего NAT-адреса.
- collision: 1 — отправитель разрешает встречное подключение, заявляя роль master для линка
- ed25519_pubkey: публичный ключ Ed25519 отправителя, 32 байта
- device_type: CLIENT_TYPE_SERVER/DESKTOP/MOBILE
- random_padding: случайные байты до размера handshake (защита от fingerprinting)
- salt + obfuscated_pubkey: публичный ключ с обфускацией (не шифруется AES,
но скрывается XOR с salt и собственным публичным ключом). Передаётся в конце пакета
в noencrypt_len байтах (не попадает под AES-шифрование)
Инициирует новый connection для tcp instance. Если tcp instance нет (первое подключение) - создаёт.
Между нодами только одно подключение, но можно добавлять каналы.
INIT устанавливает UDP-линк: создаёт или переиспользует общий ETCP_CONN к пиру.
Между узлами одно ETCP-соединение с несколькими линками; TCP-линк использует STCP handshake.
2) Init подтверждение (0x03 / 0x05):
[code(1)] [my_node_id(8 BE)] [session_id(4 BE)] [mtu_local(2)] [local_link_id(1)]
[remote_socket_id(1)] [only_local(1)] [type(1)] [peer_ipv4(4)] [peer_port(2)]
[random_padding...]
- code: 0x03 (со сбросом) или 0x05 (без сброса)
[code(1)] [node_id(8 BE)] [reset_id(8 BE)] [mtu(2 BE)] [link_id(1)]
[remote_socket_id(1)] [only_local(1)] [type(1)] [peer_ipv4(4)] [peer_port(2 BE)]
[ed25519_pubkey(32)] [device_type(1)] [keepalive(2 BE)] [random_padding...]
Фиксированное тело до padding — 64 байта (ETCP_INIT_RESPONSE_PKT).
- code: 0x03 или 0x05, вариант ответа INIT; сам по себе не сбрасывает поток клиента
- node_id: 64 бита, big-endian
- session_id: 32 бита, big-endian (должен совпасть с запросом клиента; если нет — клиент игнорирует)
- mtu_local: uint16_t локальный MTU сервера
- local_link_id: uint8_t идентификатор канала сервера
- reset_id: uint64_t, собственная локальная эпоха потока сервера (big-endian), не эхо запроса
- mtu: uint16_t локальный MTU сервера
- link_id: uint8_t идентификатор канала сервера
- remote_socket_id: uint8_t (ID серверного сокета; ID клиентского сокета известен серверу из запроса)
- only_local: uint8_t флаг сервера
- type: uint8_t тип сокета сервера
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер — для NAT traversal)
- peer_port: 2 байта (порт клиента как его видит сервер)
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер, в сетевом порядке)
- peer_port: uint16_t (порт клиента как его видит сервер, big-endian)
При IPv6 оба поля нулевые.
- ed25519_pubkey: публичный ключ Ed25519 сервера, 32 байта
- device_type: CLIENT_TYPE_SERVER/DESKTOP/MOBILE
- keepalive: uint16_t интервал keepalive сервера в мс
- random_padding: случайные байты
3) PING (0x06):
@@ -183,9 +211,9 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
ADDRESS_CHECK (0x08) — аутентифицированное входящее испытание endpoint.
Модель NAT и алгоритм владельца: doc/nat_detection.txt.
При получении init получатель пакета должен:
- reset ETCP_LINK с этим ip_port если он есть
- создать новый ETCP_CONN с этим node_id. если уже существует подключение с этим node_id - проверить ключи на совпадение.
При получении INIT получатель создаёт или переиспользует ETCP_CONN и подходящий линк;
для существующего соединения проверяет совпадение ключа пира. Эпоха из INIT учитывается
через etcp_conn_apply_peer_reset_id, без немедленного сброса потока при смене известной эпохи.
приём кодограмм из UDP выглядит так:
uasync select -> etcp_connection.c etcp_connections_read_callback: memory_pool_alloc, decrypt (or init) -> etcp_conn_input (etcp.c/h - сам tcp механизм) -> ETCP_CONN output_queue
@@ -217,14 +245,14 @@ Padding keepalive, PING/PONG и STCP-handshake:
Клиент: если все линки =down то начинается процедура восстановления связи:
- клиент посылает init без сброса (0x04), link_state=2
- сервер как получает init проверяет session_id:
- если session_id совпадает — шлёт подтверждение без сброса (0x05)
- если session_id изменился (клиент перезапустился) — шлёт подтверждение со сбросом (0x03)
- сервер восстанавливает физический линк и отвечает INIT_RESPONSE/INIT_RESPONSE_NOINIT со своей эпохой.
- прежняя эпоха пира не меняет поток; смена известной эпохи подтверждается через CHALLENGE/CONFIRM.
Код ответа 0x03/0x05 не является подтверждением смены эпохи.
**** Connection session confirmation (2026-09) ****
**** Подтверждение транспортной сессии (etcp_session.c/h) ****
Physical UDP INIT / STCP handshake establishes an authenticated link. A stream reset
uses the same control protocol on every initialized UDP or TCP link, bypassing the
normalizer and reliable DATA/ACK queues. A physical reset does not generate DOWN/UP.
normalizer and reliable DATA/ACK queues. A stream reset does not generate physical DOWN/UP.
Encrypted DATA/ACK plaintext after timestamp(2) + flag_up(1):
09 | sender_epoch:u64be | receiver_epoch:u64be | existing ACK/DATA sections
@@ -237,7 +265,8 @@ Control frames (all 25 bytes after timestamp/flag):
0c CONFIRM | sender_epoch | receiver_epoch | echoed_cookie:u64be
Each connection owns one pending peer epoch, fresh challenge, and retry timer.
HELLO and a changed epoch advertised by INIT only request a challenge. Only a
The first nonzero peer epoch from a physical handshake is stored directly if unknown.
HELLO and a change to an already known epoch advertised by INIT only request a challenge. Only a
CONFIRM for the current local epoch and pending random challenge commits a peer
epoch. Changing the peer epoch resets the stream but preserves our own epoch.
A local fatal reset generates a new local epoch and blocks DATA until confirmation.
+9 -5
View File
@@ -203,9 +203,9 @@ struct ETCP_CONN {
// Flags
uint8_t got_initial_pkt; //
uint8_t initialized; // 0 - только созданный ETCP, 1 - хотя бы один линк проинициалзирован (обмен ключами произведен)
uint8_t initialized; // 1 — поток готов; 0 — создание или reset, в том числе ожидание подтверждения сессии
uint64_t reset_id; // локальная эпоха потока (меняется только при локальном reset)
uint64_t peer_reset_id; // подтверждённая эпоха потока пира
uint64_t peer_reset_id; // эпоха пира: первая из handshake, смена — только после CONFIRM
uint64_t session_candidate, session_challenge, session_started; // эпоха-кандидат, challenge, время начала (0.1 ms)
void* session_timer; // таймер согласования транспортной сессии
uint8_t session_required; // только подтверждение challenge может завершить локальный reset
@@ -291,12 +291,16 @@ void etcp_conn_ref_free(struct ETCP_CONN* conn);
void etcp_conn_reset(struct ETCP_CONN* etcp);
void etcp_links_reset(struct ETCP_CONN* etcp);
/* Локальный reset: эквивалент fatal_reinit(); etcp — соединение, reason — причина для лога. Возврата нет. */
void etcp_conn_reinit(struct ETCP_CONN* etcp, const char* reason);
// Реинит с конкретным reset_id (для adopt чужой эпохи) — без генерации нового id
/* Сбросить поток etcp с заданной локальной эпохой reset_id; reason — причина для лога. Возврата нет.
* Не генерирует эпоху и не запускает подтверждение. При подтверждённой смене эпохи пира передают текущий etcp->reset_id. */
void etcp_conn_reinit_id(struct ETCP_CONN* etcp, const char* reason, uint64_t reset_id);
// Фатальный ресет (normalizer desync / bad fragment size): генерит новую эпоху reset_id
/* Сгенерировать новую локальную эпоху, сбросить поток etcp и запустить подтверждение сессии.
* reason — причина для лога; DATA ждёт подтверждения, физические линки сохраняются. Возврата нет. */
void etcp_conn_fatal_reinit(struct ETCP_CONN* etcp, const char* reason);
// Применение чужого reset_id: при отличии — slave принимает эпоху пира и ресетится, master пересылает свою
/* Учесть эпоху peer_reset_id из handshake соединения conn: первую сохранить, смену предложить для challenge.
* Нулевая и прежняя эпохи игнорируются. Сброс при смене выполняется после CONFIRM, своя эпоха сохраняется. Возврата нет. */
void etcp_conn_apply_peer_reset_id(struct ETCP_CONN* conn, uint64_t peer_reset_id);
void etcp_connection_ready(struct ETCP_CONN* etcp);// вызывается когда подключение инициализировано
+5 -4
View File
@@ -79,13 +79,14 @@ struct ETCP_DGRAM {// пакет (незашифрованный)
// Размер базового ACK: type(1) + count(1) + last_delivered_id(4) + rx_dup_count(2)
#define ETCP_ACK_BASE_SIZE (2 + 4 + 2)
// --- INIT packet wire formats (inside encrypted payload, before padding) ---
// INIT внутри шифрованного тела, до padding: запрос — 67 байт, ответ — 64 байта.
// Каждая сторона передаёт свою эпоху reset_id; смена известной эпохи подтверждается через etcp_session.
// INIT REQUEST: client → server
struct ETCP_INIT_REQUEST_PKT {
uint8_t code; // 0: ETCP_INIT_REQUEST (0x02) или ETCP_INIT_REQUEST_NOINIT (0x04)
uint8_t node_id[8]; // 1: sender node_id (big-endian)
uint8_t reset_id[8]; // 9: reset epoch id (big-endian)
uint8_t reset_id[8]; // 9: локальная эпоха потока клиента (big-endian)
uint8_t mtu[2]; // 17: client MTU (big-endian)
uint8_t keepalive[2]; // 19: keepalive interval (big-endian)
uint8_t recovery[2]; // 21: recovery interval/100 (big-endian)
@@ -108,10 +109,10 @@ struct ETCP_INIT_REQUEST_PKT {
struct ETCP_INIT_RESPONSE_PKT {
uint8_t code; // 0: ETCP_INIT_RESPONSE (0x03) или ETCP_INIT_RESPONSE_NOINIT (0x05)
uint8_t node_id[8]; // 1: server node_id (big-endian)
uint8_t reset_id[8]; // 9: reset epoch id (big-endian)
uint8_t reset_id[8]; // 9: локальная эпоха потока сервера (big-endian), не эхо запроса
uint8_t mtu[2]; // 17: server MTU (big-endian)
uint8_t link_id; // 19: server's local link id
uint8_t remote_socket_id; // 20: echo of client's socket_id
uint8_t remote_socket_id; // 20: ID серверного сокета, отправляющего ответ
uint8_t only_local; // 21: server only_local flag
uint8_t type; // 22: server socket type (CFG_SERVER_TYPE_*)
// V2 fields: