Формат кодограммы: ------------------ Кодограмма сервиса через 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 и восстанавливают операции своего протокола; при невозможности согласования соединение закрывается.