diff --git a/doc/etcp_protocol.txt b/doc/etcp_protocol.txt index de4620d4..6663f769 100644 --- a/doc/etcp_protocol.txt +++ b/doc/etcp_protocol.txt @@ -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. diff --git a/src/transport_layer/etcp.h b/src/transport_layer/etcp.h index 6649a226..82eb2e02 100644 --- a/src/transport_layer/etcp.h +++ b/src/transport_layer/etcp.h @@ -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);// вызывается когда подключение инициализировано diff --git a/src/transport_layer/etcp_connections.h b/src/transport_layer/etcp_connections.h index 0ec1d312..0fa187f3 100644 --- a/src/transport_layer/etcp_connections.h +++ b/src/transport_layer/etcp_connections.h @@ -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: