Files
utun2/doc/etcp_protocol.txt
T

442 lines
54 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Формат кодограммы:
------------------
Кодограмма сервиса через etcp_send попадает в normalizer; его фрагменты поступают в
input_queue, затем в indexed input_send_q с назначенным seq.
Очереди, ACK/RTO, изменение окна и готовность линка вызывают etcp_conn_wake_sender.
Один call_soon обслуживает накопленные события без рекурсивного входа и задержки таймером.
При OOM выделения сетевого пакета очередь сохраняется; retry назначается через 10ms.
close/reset отменяет ожидающий запуск. Не подтверждённая сессия не передаёт DATA/ACK;
HELLO/CHALLENGE/CONFIRM и keepalive передаются своим независимым путём.
Формирование пакета:
1. Просмотреть input_send_q без извлечения: первый готовый повтор, иначе первый готовый новый DATA.
Заблокированная запись пропускается; порядок остальных записей не меняется. Поиск не резервирует окно,
не выдаёт номер попытки и не меняет историю/RR. Если кандидата нет, можно подгрузить один новый фрагмент
и повторить просмотр. Общий бюджет учитывает DATA-байты send_q+wait_ack; приёмное окно по till сохраняется.
После callback подгрузки проверяются reset_count/close/сессия, включая удаление прежнего burst-линка.
2. Выбрать инициализированный UP-линк с готовым shaper и местом для целого DATA:
inflight_bytes + длина DATA <= inflight_lim_bytes. Проверка выполняется без переполнения.
Среди допустимых выбирается минимум inflight_bytes, при равенстве — round-robin.
Для повтора обход начинается после last_link самого фрагмента: сначала неиспользованные допустимые пути.
История — битовая карта256 локальных слотов,32 байта на логический пакет; wire-формат не меняется.
Пока остался неиспользованный путь, занятый по cwnd/pacing, этот пакет ждёт, а send_q просматривается дальше.
Когда все допустимые пути использованы, разрешён следующий обход. Очистка истории и новый бит фиксируются
только после принятия send; отказ send не расходует обход. Перед удалением линка очищаются его курсор/бит
во всех логических записях до освобождения слота и callbacks, которые могут повторно выдать тот же local_link_id.
При наличии незастрявшего initialized+UP пути застрявшие исключены из повторов, даже если альтернатива занята.
Если застряли все, доступны все UP-пути. Прежний путь разрешён лишь при отсутствии других допустимых;
иначе ждём альтернативу. DOWN последней альтернативы пробуждает отправителя.
3. Наступивший по сроку или срочный ACK обслуживает тот же линк, который принял DATA,
без ограничения DATA-cwnd/pacing. Готовый DATA другого линка уступает этому ACK;
DATA того же линка забирает ACK piggyback. Если DATA не помещается, он остаётся в очереди.
Кумулятивный till можно отправить по любому рабочему пути. ACK-only не увеличивает нагрузку DATA-shaper;
физическая очередь канала остаётся общей, её накопление ограничивают DATA-cwnd/pacing BBR.
4. Выделить ETCP_DGRAM до удаления DATA из очереди и изменения счётчиков.
Выделить физическую попытку с новым attempt_no для этого seq и зарезервировать окно выбранного линка.
Только успешное принятие транспортом фиксирует время, last_link, историю/RR и перенос DATA в input_wait_ack.
Ошибка send снимает резерв; логический DATA остаётся готовым без сетевого loss sample.
Пакет содержит ACK, опциональные timestamp/измерительные секции и последнюю секцию PAYLOAD.
5. В активной ограниченной burst-серии pacing обходится; DATA всё равно должен
помещаться в окно, иначе серия отправляет FILLER. ACK/FILLER окно DATA не занимают.
Если готовый повтор относится к другому пути, серия идёт FILLER: новые DATA его не обгоняют.
При приёме пакета (от etcp_connections):
- последовательно сканируем секции и отдаём их на обработку нужным обработчикам:
- ack (0x01): пара seq+attempt_no подтверждает физическую попытку только принявшего ACK линка;
seq отдельно подтверждает логическую доставку. till освобождает логические записи, без BBR/снятия чужого flight.
- timestamp (0x06): обновляем RTT и EWMA |ΔRTT| пути; recv_dt_avg хранит поправки времени TX/RX в Q16.
У conn rtt_last — среднее RTT линков, rtt_avg_10 — их максимум (имя историческое).
BBR использует отдельные DATA RTT/jitter по ACK физических попыток; RTO также учитывает rtt_last своего линка.
- [0x00] payload: сохраняем фрагмент в recv_q либо распознаём уже сохранённый/доставленный seq.
Только после этого ставим ACK с seq+attempt_no в ack_q принявшего линка; OOM до сохранения не подтверждается.
после сканирования проверяем появились ли в linked-list собранные данные которые можно переместить в выходную очередь. и перемещаем если есть.
Таймер попыток каждого линка (etcp_tx.c):
- RTO = clamp(ceil(1.2*RTT_for_RTO + 1.5*data_jitter_us + 3000), 10000, 1000000) мкс,
RTT_for_RTO=max(data_rtt_us,100*rtt_last): DATA EWMA и последний timestamp/keepalive RTT своего линка.
Целочисленный расчёт использует uint64 и округляет вверх до целой микросекунды.
RTT использует собственный DATA ACK минус receiver_delay; EWMA RTT=1/8, jitter=1/4 |RTT-EWMA|.
Первый DATA RTT задаёт jitter=RTT/2. Timestamp-RTT задаёт нижнюю границу срока, чтобы после роста задержки
попытки доживали до ACK и DATA-измерения продолжались; BBR RTT/min_rtt по этому сигналу не обновляются.
Без обоих измерений RTO=1с.
- FIFO содержит физические попытки в порядке принятия транспортом. Таймер назначается по старейшей;
при изменении RTO проверяется новый срок. Timestamp handler пересчитывает таймер сразу после обновления RTT,
включая сокращение без новых DATA/ACK. Допускается оставить уже запланированную более раннюю проверку.
uasync округляет задержку вверх до миллисекунды; callback может выполниться позже расчётного срока.
- Истечение снимает только собственный физический flight и сообщает loss собственному BBR.
Если логический seq ещё не доставлен и других активных копий нет, он возвращается в input_send_q.
Уже доставленный seq не возвращается, даже если его физическая копия истекла позже.
Обычный ACK-only ждёт received_at_us старейшей записи своего пути + 2000мкс.
Piggyback на готовом DATA передаёт ACK сразу. Готовый ACK может включать более свежие записи:
срок проверяется по голове пакета, а не отдельно для каждой записи внутри него.
Среди готовых обычных ACK выбирается самый старый срок; срочный ответ на ACK_REQUEST имеет приоритет.
Короткая ограниченная измерительная burst-серия завершается до перехода к ACK другого пути.
Продвижение логической границы также задаёт срок 2мс от первого неподтверждённого продвижения;
следующие DATA его не отодвигают. Любой принятый пакет с актуальным till снимает этот запрос.
Один ack_resp_timer соединения обслуживает минимум сроков очередей рабочих путей и логической границы.
После piggyback/частичной отправки срок пересчитывается по оставшимся головам. Новый RX между
timer и отложенным sender не задерживает старые ACK: готовность проверяется по абсолютному времени.
При DOWN очередь пути не создаёт таймер; после UP её старый наступивший срок действует сразу.
OOM ACK-таймера логируется и включает существующий срочный ответ, сохраняя received_at_us для RTT.
Reset/close отменяют таймер и очищают сроки; DATA/RTO/BBR и wire-формат этим механизмом не меняются.
inflight - две очереди (ll_queue):
- input_send_q: список пакетов ожидающих отправку
- input_wait_ack: список неподтвержденных пакетов ожидающих ack
При принятии отправки транспортом пакет перемещается из input_send_q в input_wait_ack.
При необходимости повтора пакет перемещается из input_wait_ack обратно в input_send_q.
доп. свойства пакетов (struct INFLIGHT_PACKET):
- указатель на ETC_LINK последней отправки (last_link)
- timestamp последней отправки (last_timestamp)
- число отправок (send_count)
- send_hist[8] — local_link_id последних попыток, только диагностика
- last_attempt_id — внутренний составной ключ (seq<<16)|attempt_no последней принятой отправки
- attempt_count — выданные номера попыток данного seq, общие для всех каналов, включая неудачные send
- active_attempts — число ещё учитываемых физических копий
- Payload принадлежит только логической записи. Попытка ETCP_TX_ATTEMPT хранит seq и неизменные снимки, без payload.
inflight_bytes/inflight_packets линка равны сумме DATA-длин/числу записей в его attempts, включая резерв send.
ACK/till может уже удалить логический объект, пока другая физическая копия продолжает занимать своё окно.
Повторный ACK неизвестной/истёкшей попытки не вычитает flight и не создаёт BBR sample.
ACK известного ID с другим seq отклоняется целиком; неизвестный ID допускает только логическое подтверждение seq.
Удаление линка обнуляет last_link в обеих очередях до освобождения его памяти.
Общий optimal_inflight равен сумме лимитов всех линков, с насыщением UINT32_MAX;
это дополнительный backpressure подачи новых фрагментов, не замена индивидуальных окон.
Отдельно ограничиваем размах sequence относительно кумулятивного ACK: новый seq выдаётся
только если (uint32_t)(next_tx_id - rx_ack_till) <= MAX_INFLIGHT_SIZE (16384).
Selective ACK освобождает отдельную inflight-запись, но не продвигает эту границу:
приёмник пока не может освободить пакеты за дыркой. Кумулятивный ACK возобновляет
input_queue даже если подтверждённые записи уже удалены по selective ACK.
При заполненном sequence-окне собственный таймер запрашивает актуальный till секцией ACK_REQUEST.
Получатель отвечает через запрашивающий путь, даже без ожидающих DATA/ACK-элементов.
При продвижении frontier, reset/close таймер отменяется. Это восстанавливает поток при потере последнего till.
Память физических попыток ограничена: 16384 на линк, 32768 на соединение; исчерпание блокирует DATA до освобождения.
Очередь ACK каждого линка ограничена 16384 записями; переполнение логируется, DATA остаётся сохранённым для повтора/till.
ACK ограничивается последним назначенным seq (next_tx_id - 1); сравнения учитывают uint32 wrap.
BBR каждого линка получает ACK/loss samples и возвращает cwnd и pacing rate.
Скорость — доставленные байты за max(интервал отправки, интервал доставки), по снимкам при отправке.
Интервал между соседними ACK не используется: ACK одной пачки могут обрабатываться в один tick.
ACK с подтверждениями также возвращает точку приёма своего пути: накопленные DATA-байты и монотонное время
последнего увеличения счётчика. В счётчик входят допустимые сохранённые/уже доставленные DATA, включая копии;
OOM до сохранения и отвергнутые DATA его не меняют. Считаются байты полезной нагрузки секции, без заголовков.
При отправке попытка запоминает известную точку. ACK действующей попытки продвигает точку пути;
повторный/чужой ID её не меняет, переставленные ACK не откатывают. Противоречивая пара логируется и игнорируется.
При положительных дельтах interval=max(send_us,ceil(delivered*delta_rx_us/delta_rx_bytes)).
Приёмный интервал заменяет ACK clock: обратная очередь больше не завышает BW сжатием ACK
и не занижает его растяжением. Send-интервал сохраняет защиту от сжатия приёмного времени.
Без достоверной приёмной базы/положительных дельт используется max(send_us,ack_us).
Счётчики и времена — uint64, часы двух сторон не сравниваются; нужны только дельты одних часов.
Если интервал не представим в uint32, sample не используется для BW вместо насыщения, завышающего скорость.
После пустого физического flight база начинается заново, чтобы простой не занижал скорость следующей передачи.
Reset потока обнуляет точки приёма; исчерпание локального 64-битного счётчика закрывает соединение с ERROR.
RTT/min_rtt, формула RTO, политика окна и дедлайн ACK не корректируются приёмной точкой.
Внутренняя полоса — байты/мкс Q16; pacing — байты/с, BDP — байты, без повторного множителя MSS.
Минимальное окно BBR — четыре DATA-фрагмента normalizer; максимальное — bbr_max_cwnd (по умолчанию 1048576).
8192 — нижняя допустимая граница настройки bbr_max_cwnd; транспорт не повышает до неё текущее окно.
PROBE_RTT осушает flight до max(4*MSS, 0.5*BDP), выдерживает >=200мс и раунд ACK, затем восстанавливает окно.
RTT-фильтры обновляются при обработке ACK. Короткий минимум принимает меньший достоверный RTT
либо текущее измерение после истечения 5с; RTT=0 означает отсутствие измерения.
Долгий min_rtt принимает короткий минимум, если он не больше прежнего, либо истекли его 10с.
Вместе со значением копируется метка короткого фильтра: повторное использование кеша не задаёт текущее время.
Завершение PROBE_RTT возобновляет короткое окно. Окна истекают строго после своей границы;
hold завершается при достижении срока и завершённом ACK-раунде. Метки RTT и срок hold — полные uint64 TB (100мкс).
MSS — размер DATA-фрагмента normalizer без заголовков; начальное окно UDP и STCP равно 4*MSS с верхним лимитом конфига.
В PROBE_BW действует дополнительный целевой бюджет очереди 20мс: всё окно, включая резервы ACK и PROBE_UP,
ограничивается max(4*MSS, BDP + BW*20мс) по min_rtt. STARTUP/DRAIN не ограничивают окно этой формулой,
но целый раунд RTT выше min_rtt+бюджет завершает STARTUP и переводит его в DRAIN.
Shaper списывает только DATA-байты: единицы совпадают с delivery sample и pacing BBR.
Это цель по оценкам BBR; минимум окна, изменение полосы и очереди обратного пути могут давать большую задержку.
Отказ из-за нехватки окна сохраняет cwnd_limited до следующего ACK: вычитание ACK
не должно скрывать этот факт от BBR. app_limited учитывает ожидающие очереди и
pending/частичный буфер normalizer; ограничение окном не означает отсутствие данных.
Потеря для BBR учитывается при RTO, фактическая повторная передача — при новой попытке.
Три RTO физических DATA-попыток без ACK-прогресса ставят локальный флаг tx_stalled, в том числе в одном вызове таймера.
До установки флага действующий ACK своего пути сбрасывает серию RTO. После установки сброс возможен только по ACK
попытки, принятой транспортом после момента установки флага; старый/чужой/уже снятый attempt не доказывает восстановление.
tx_stalled ограничивает выбор повторов при наличии альтернативы; UP, новые DATA, ACK/keepalive и формула RTO сохраняются.
Reset линка снимает флаг и серию RTO. Новых очередей, копий payload и таймеров детектор не создаёт.
При уменьшении окна уже отправленные пакеты остаются в учёте; новые DATA ждут ACK/RTO.
Измерительные секции burst 0x07/0x08 и FILLER поддерживаются реализацией.
**** Формат кодограмм для etcp.c/h ****
Каждая кодограмма состоит из обязательного заголовка и опциональных секций:
1. Обязательный заголовок всего пакета (3 байта) добавляется при передаче, есть во всех пакетах:
[Timestamp:2][flags/flag_up:1]
- Timestamp: uint16_t, время отправки в единицах 0.1 мс (циклическое), при повторе обновляется.
Копируется из памяти в порядке байтов хоста; на используемых little-endian платформах передаётся LE.
- flag_up: uint8_t, bit0 = up/down (recv_keepalive) — признак живой приёмной стороны
шифруется строго ВСЁ включая ВСЕ заголовки (кроме обфусцированного publickey в INIT-пакетах).
timestamp вставляется ВСЕГДА ВО ВСЕ КОДОГРАММЫ (скрыто в шифрованном заголовке).
Для 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)][attempt_no(2 LE)][delay_us(4 LE)] × count]
- count: количество ACK записей (1 байт; до 255, дополнительно ограничено MTU и оставшимся местом)
- last_delivered_id: uint32_t — последний ID, доставленный получателю (по этот ID всё собрано подряд)
- rx_dup_count: uint16_t — счётчик принятых дубликатов (локальный, для вычисления tx_dup_count на удалённой стороне)
- seq: uint32_t — номер подтверждаемого пакета
- attempt_no: uint16_t — номер физической попытки данного seq, 1..65535; возвращается только тем же путём
- delay_us: uint32_t — задержка между приёмом DATA и формированием ACK, мкс; UINT32_MAX исключает RTT sample
При count>0 после всех записей обязательно добавляется [rx_data_bytes(8 LE)][rx_data_time_us(8 LE)].
Это точка приёма всего пути на момент формирования ACK, а не отдельной подтверждаемой попытки.
Время — монотонные микросекунды получателя при последнем положительном приросте DATA-байтов; 0/0 — точки нет.
Каждая запись = 10 байт, база ACK = 8 байт; полный размер = 8+10*count+(count>0 ? 16 : 0).
count=0 допустим для передачи till, точки приёма не содержит. До обработки проверяется вся длина ACK.
ACK_REQUEST: [0x0a], одна секция длиной 1 байт, запрос повторной отправки актуального till.
в) channel timestamp (добавляется после выбора канала передачи, если есть свежие данные):
[0x06] [RET_Timestamp(2 байта LE)] [RECV_Rel_Timestamp(2 байта LE)]
- RET_Timestamp: uint16_t = last_recv_timestamp + (now - last_recv_local_time)
(оценка текущего времени на передающей стороне в момент отправки)
- RECV_Rel_Timestamp: uint16_t = last_recv_local_time - last_recv_timestamp
(относительное смещение между локальным и удалённым временем)
Всего 5 байт. Добавляется только если last_recv_updated=1 (был принят пакет с момента последней отправки)
и если прошло менее 100с (dt < 1000000 таймбаз). Результат rtt_last задаёт нижнюю границу RTT для RTO;
DATA RTT и min_rtt BBR обновляются только ACK действующих физических попыток.
3. Полезная нагрузка - заголовок 0x00 (одна секция и всегда последняя):
[0x00] [ID 4 байта LE] [attempt_no 2 байта LE] [данные...]
- ID: uint32_t, номер фрагмента 1..UINT32_MAX в пределах одной эпохи потока.
- attempt_no: uint16_t, номер 1..65535, общий для всех каналов одного ID.
Номер выдаётся при создании попытки и не возвращается при отказе send; исходный seq при повторе сохраняется.
После номера65535 следующая попытка аварийно меняет эпоху через REINIT; счётчик не оборачивается.
Перед выдачей seq=0 после UINT32_MAX поток также меняет эпоху: пара seq/attempt_no не повторяется внутри эпохи.
Смена эпохи отбрасывает прежний поток, владельцы восстанавливают свои операции по REINIT, как при фатальном OOM.
Старые DATA/ACK отклоняются заголовком сессии до разбора. Заголовок PAYLOAD занимает7 байт.
Внутренние хеш-очереди сохраняют64-битный ключ (seq<<16)|attempt_no; он не передаётся по сети целиком.
Пакет может содержать несколько опциональных секций (например, ACK + channel_timestamp + данные),
Порядок: ACK, возможный ACK_REQUEST, измерительные/временные секции и FILLER, затем payload последним.
Разбор секций — последовательное сканирование с известными размерами (нет явного поля длины секции).
цель протокола:
1. контролировать состояние очередей (не накапливать данные где этого можно избежать), анализировать RTT и inflight - динамически подстраивать размер окна и ретрансмиссии, утилизируя сеть насколько это возможно но не допуская наполнения буферов в сети (что приводит к бестолковому увеличению RTT)
Установка подключения для канала (реализуется в etcp_connections.c/h):
каждое ETCP подключение может содержать несколько линков до endpoint (разные маршруты, каналы связи).
При прохождении трафика через этот модуль подсчитываются метрики (для каждого канала отдельно):
- RTT последнего пакета и время отправки последнего пакета
- общее количество отправленных/принятых байт
- число фактических повторных отправок; потери для BBR учитываются отдельно при истечении физических попыток.
- обновление текущего количества неподтвержденных данных inflight (в пакетах и байтах) - сколько по этому каналу отправленных и неподтвержденных на текущий момент
Планировщик/балансир каналов (etcp_loadbalancer.c/h):
Окно и pacing проверяются до извлечения DATA из очереди; без допустимого UP-линка фрагмент остаётся в очереди.
У каждого линка собственный shaper по точной bbr_pacing_rate в байтах/с:
- После успешного send списываются только payload_len DATA-байтов, без заголовков и ACK.
- Основной счётчик времени имеет единицу 0.1 мс; sub-разряды — 0.1 нс, модуль 1000000.
Прибавка равна ceil(payload_len * 10000000000 / bbr_pacing_rate), вычисляется целочисленно в uint64.
- Накопленный кредит после простоя ограничивается 1 мс. При смене ставки пересчитывается оставшийся долг;
таймер снимается и при необходимости назначается заново. При нулевой ставке pacing не ограничивает отправку.
- Положительный долг должен быть строго меньше 1 мс. Таймер ждёт первого допустимого TB:
shaper_load_time_tb - SHAPER_BURST_DELAY_TB + 1; дробь меньше TB не меняет эту строгую границу.
uasync округляет срок вверх до миллисекунды; callback повторно проверяет готовность перед уведомлением.
- ACK/control и измерительная burst-серия не списывают DATA-pacing. Окно DATA проверяется и в burst.
Конструктор исходящего UDP-линка запускает INIT сразу; TCP использует отдельный STCP connect/handshake.
процесс установки соединения (рукопожатие):
- клиент отправляет init запрос и выставляет таймаут
- по таймауту повторяет INIT: начальный интервал 500 TB = 50 мс, максимум 50000 TB = 5 с;
каждые 10 отправок интервал увеличивается на interval/4 + 1 TB. TB здесь равен 0.1 мс.
- когда клиент получает init подтверждение — снимает таймаут, выставляет link_state=connected, запускает keepalive.
- каждая сторона сообщает в 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 (начальный 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)] [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)]
Фиксированное тело до padding — 67 байт (ETCP_INIT_REQUEST_PKT).
- code: 0x02 или 0x04, вариант handshake линка
- node_id: 64 бита, идентификатор узла (big-endian)
- 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 (1-255), назначается отправителем; ноль запрещён
- socket_id: uint8_t, уникальный ID сокета отправителя
- only_local: uint8_t, флаг "только локальные подключения"
- type: uint8_t, конфигурационный тип сокета CFG_SERVER_TYPE_*
- 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: ключ отправителя XOR SHA256(salt || публичный ключ получателя).
Этот блок не шифруется AES, передаётся в конце пакета
в noencrypt_len байтах (не попадает под AES-шифрование)
INIT устанавливает UDP-линк: создаёт или переиспользует общий ETCP_CONN к пиру.
Между узлами одно ETCP-соединение с несколькими линками; TCP-линк использует STCP handshake.
2) Init подтверждение (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
- reset_id: uint64_t, собственная локальная эпоха потока сервера (big-endian), не эхо запроса
- mtu: uint16_t локальный MTU сервера
- link_id: uint8_t идентификатор канала сервера, 1..255
- remote_socket_id: uint8_t (ID серверного сокета; ID клиентского сокета известен серверу из запроса)
- only_local: uint8_t флаг сервера
- type: uint8_t тип сокета сервера
- 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):
[0x06] [flags(1)] [node_id(8 BE)] [nonce(8 BE)] [user_data_len(2 BE)] [user_data...] [padding...]
[salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
Отправляется с отдельным crypto-контекстом (не связан с ETCP-сессией).
Используется для проверки доступности узла и NAT-тестирования.
4) PONG (0x07):
[0x07] [flags(1)] [node_id(8 BE)] [nonce(8 BE)] [resp_data_len(2 BE)] [response_data...] [padding...]
[salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
Ответ на PING. nonce копируется из запроса для сопоставления.
Nonce генерируется CSPRNG. Проверяются также ключ отправителя, source IP:port,
ID и поколение принимающего сокета. Флаг OBSERVE (0x04) запрашивает ответ
address(19) || tag(25): family(4/6), IP(16), port(2, BE), NAT2UDP!(8),
socket_id(1), generation(8, BE), round(8, BE).
ADDRESS_CHECK (0x08) — аутентифицированное входящее испытание endpoint.
Модель NAT и алгоритм владельца: doc/nat_detection.txt.
До изменения транспорта INIT_REQUEST проверяет полное 67-байтовое тело и соответствие node_id
аутентифицированному X25519-ключу. Оба направления INIT отклоняют link_id=0 и MTU меньше
ETCP_MIN_UDP_MTU(family): 191 байт для IPv4, 211 для IPv6. MTU больше 1500 ограничивается сверху.
Получатель создаёт или переиспользует ETCP_CONN и подходящий линк;
для существующего соединения проверяет совпадение ключа пира. Эпоха из INIT учитывается
через etcp_conn_apply_peer_reset_id, без немедленного сброса потока при смене известной эпохи.
Принятый INIT обновляет recv_keepalive, время последнего RX и входной remote_keepalive=flag_up
до переиспользования буфера для ответа. INIT с flag_up=0 снимает прежний UP UDP-линка:
получение запроса доказывает только входящую достижимость, обратный путь ещё не подтверждён.
Ответ и keepalive отправляются и при DOWN, чтобы холодное подключение и восстановление
могли завершить двустороннюю проверку. При DOWN одного линка соединение остаётся UP,
если есть другой работоспособный линк. Повтор неизменного состояния не создаёт нового DOWN/UP.
INIT_RESPONSE должен содержать полное обязательное тело (64 байта без timestamp/flag_up)
и node_id, соответствующий аутентифицированному ключу пира. Усечённый ответ или чужой node_id
отклоняются до изменения живости, эпохи и параметров линка. Дополнительный padding разрешён.
Входящий conn с известным ключом индексируется по node_id до callback создания линка.
Отмена входящего линка из callback не закрывает conn, если у него появился другой линк или NCD-владелец.
Close немедленно отключает линк и ресурсы, но память удерживается active_users до выхода внутренних стеков RX/переходов/callbacks.
Приём UDP: uasync -> etcp_connections_read_callback_socket -> etcp_process_packet ->
etcp_packet_decrypted (проверки и снятие SESSION_DATA) -> etcp_conn_input -> normalizer/output_queue.
Keepalive: [0x08] [flags(1)] [period_ms(2 LE)] [sleep_fields?:4] [padding...].
При KA_FLAG_SLEEP добавляются active_remaining(2 LE) и sleep_duration(2 LE), оба в единицах 0.1 с.
Полезное тело занимает 4 байта без SLEEP или 8 байт с SLEEP; остальные байты — padding.
Обычные keepalive периодически шлют обе стороны; полезный TX сбрасывает адаптивный период к базовому.
Сон одной из сторон останавливает обычный таймер. UDP-анонс сна подтверждается флагом SLEEP_ACK,
повторяется до ACK/исчерпания попыток; TCP отдельного подтверждения сна не требует.
RX watchdog работает отдельно в normal и standby и не продлевается собственными отправками.
Сервер прекращает слать keepalive если линк потерян (ждёт keepalive от клиента).
flag_up в заголовке устанавливается в значение recv_keepalive (признак что локальная сторона принимает пакеты).
Padding keepalive, PING/PONG и STCP-handshake:
- ETCP_CONN хранит собственный диапазон min/max и независимый таймер для всех своих линков.
- UTUN_INSTANCE хранит отдельный диапазон и таймер для PING/PONG без существующего ETCP_CONN.
- При создании владельца и каждом обновлении: min=rand(0,30), max=min+rand(32,128), границы включительные.
- Следующее обновление назначается через rand(50,300) секунд. При закрытии владельца таймер отменяется.
- Каждый пакет независимо выбирает длину rand(min,max); padding заполняется нулями ДО шифрования.
- UDP PING/PONG: padding шифруется перед CRC/tag; открытый salt+pubkey остаётся в конце датаграммы.
user_data_len/resp_data_len обозначают только полезные данные. PONG не копирует padding запроса.
- STCP: salt(8)+pubkey(32) открыты, затем 51 байт зашифрованного handshake и padding_size байт
нулевого padding, зашифрованного продолжением того же CTR. Приём продвигает CTR через padding;
padding_size=0 сразу завершает handshake. При ETCP_CONN клиент читает актуальный диапазон при отправке;
без ETCP_CONN сохраняет выбранную длину при запуске. Сервер выбирает длину независимо по пиру/instance.
- Каждый STCP-поток использует соль своего отправителя: клиентскую для client→server, серверную для server→client.
CTR IV = первые 12 байт SHA256(session_key || stream_id_LE || salt(8) || "uTun3-stream") + четыре нулевых байта.
Отправка инициализируется после генерации своей соли, приём — после чтения соли пира, до расшифровки тела.
После handshake CTR непрерывно продолжается через padding и DATA, без повторной инициализации на кадр.
- Padding не должен превышать 158 байт. Для UDP длина ограничивается оставшимся MTU;
если не помещается даже min, отправка завершается ошибкой без обрезания пользовательских данных.
- Диагностика диапазонов/обновлений/PING/PONG: etcp=debug; keepalive: keepalive=debug.
Клиент: если все линки =down то начинается процедура восстановления связи:
- клиент посылает init без сброса (0x04), link_state=2
- сервер восстанавливает физический линк и отвечает INIT_RESPONSE/INIT_RESPONSE_NOINIT со своей эпохой.
- прежняя эпоха пира не меняет поток; смена известной эпохи подтверждается через CHALLENGE/CONFIRM.
Код ответа 0x03/0x05 не является подтверждением смены эпохи.
**** Подтверждение транспортной сессии (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 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
The extra 17 bytes participate in the MTU budget. Unwrapped DATA/ACK and frames with
an epoch mismatch are discarded before stream state or link liveness is updated.
Control frames (all 25 bytes after timestamp/flag):
0a HELLO | sender_epoch | receiver_epoch | 0
0b CHALLENGE | sender_epoch | receiver_epoch | random_cookie:u64be
0c CONFIRM | sender_epoch | receiver_epoch | echoed_cookie:u64be
Each connection owns one pending peer epoch, fresh challenge, and retry timer.
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.
Both sides may reset concurrently. Loss of any control frame is recovered by a
500 ms retry on all initialized links. After 10 s an unconfirmed local reset closes
the connection; an unconfirmed candidate alone is discarded without closing a
working session. Close cancels the timer before destroying links.
TCP may initially receive peer epoch zero because the server creates ETCP_CONN
only after STCP handshake. It completes the same session confirmation before INIT/UP.
REINIT notifies stream consumers without tearing down physical ownership. The
router retains end-to-end inflight; BGP repeats JOIN_GROUP and TABLE_REQUEST because
its previous control messages may have been discarded by the stream reset.
Отказы памяти и прерывание потока:
- OOM до сохранения RX DATA в recv_q не создаёт ACK; отправитель повторяет DATA.
- ACK означает принятие в текущий транспортный поток, а успешный etcp_send —
передачу владения транспорту. Оба не гарантируют завершение операции сервиса
после прерывания сессии. Надёжность операции через REINIT обеспечивает её владелец.
- OOM после принятия данных в normalizer или при подготовке TX inflight вызывает
fatal REINIT: новая локальная эпоха, блокировка DATA, отбрасывание старых очередей,
частичных кодограмм и физических попыток. Старые payload не воспроизводятся.
Физические линки сохраняются; очистка очередей/reset не выделяет память.
- Активный обработчик освобождает собственный извлечённый фрагмент ровно один раз;
после reset/close из callback он прекращает разбор старой кодограммы/пакета.
- Согласование новой эпохи выполняется после очистки. OOM control-пакета оставляет
повтор активным, без продления исходного срока 10с. Отказ обязательного session-
таймера или генерации локальной эпохи закрывает соединение с диагностикой.
Если невозможно выделить даже обязательный deferred-close token внутри callback,
процесс завершается через abort: соединение не оставляется в зависшем состоянии.
- После подтверждения эпох владельцы получают REINIT и восстанавливают операции
своего протокола; при невозможности согласования соединение закрывается.