Merge remote-tracking branch 'origin/master'

This commit is contained in:
evgeny committed 2026-10-24 17:15:42 +02:00
commit 80558f6686
30 files changed
+1808 -201

No files matched your search

+26 -3
View File
@@ -45,8 +45,26 @@ Nonce метаданных использует `part=UINT64_MAX`; файл —
Получатель проверяет подписанный SHA256 полного контейнера, UUID, tag метаданных,
все tags файла, точный размер и hash тела. Результат пишется во временный файл
и публикуется после успешной проверки и durable rename. Все длительные файловые
операции и криптография выполняются в media_async.
и публикуется после успешной проверки и durable rename. Подготовка файлов
и криптография выполняются в media_async worker.
`encrypted_file_write/read` возвращают закрытый проверенный `file_io_temp`,
не изменяя конечное назначение. Задача владеет им до publish/discard; запись идёт
в эксклюзивно созданный соседний файл с правами 0600 на POSIX. Одинаковые файлы,
включая `/./`, hardlink и конечную ссылку назначения, отклоняются до подготовки;
публикация повторно проверяет идентичность. При ошибке read не выдаёт новые метаданные.
Режим `FILE_IO_DURABLE` действует весь срок temp: flush/sync открытого файла/close,
затем rename и сохранение каталога. После успешного rename ошибка directory sync
не откатывает установленный результат; temp уже отсоединён от владельца.
Completion не удаляет файлы по заранее вычисленному имени: очистка учитывает,
какие артефакты задача действительно создала или опубликовала.
Реестр `file_io` защищает активные temp от очистки кеша, включая время после close.
Восстановление до запуска workers и очистка после их завершения удаляют оставшиеся
`<UUID>.uenc.tmp.<UUID>` / `<UUID>.file.tmp.<UUID>` в `media/decrypted`.
Конечные файлы и имена других форматов этот проход сохраняет; активные temp другого
instance того же процесса остаются защищены общим реестром.
## Хранение и разрешения
@@ -110,9 +128,14 @@ backpressure и повтором через 5 секунд; restart продол
`test_encrypted_file`: границы порций, пустой файл, metadata roundtrip,
неверные ключ/автор/UUID/hash, подмена метаданных с пересчитанным внешним hash,
лишние байты и отсутствие частичного plaintext после отказа.
лишние байты, alias обоих API, сохранность существующего назначения/метаданных,
23 сценария отказов I/O с контролем fd/temp, включая ошибку после rename.
`test_media_primitives` проверяет оба режима temp и границу durable-публикации;
`test_attachment_send` останавливает сервис после настоящего crypto rename до completion.
`test_encrypted_attachment`: реальные A–S–B/C, скачивание с S при выключенном A,
перезапуски автора с запросами и получателя с ключом, выдача B и отказ C,
отсутствие ключа на S/C, отрицательные control-пакеты, повтор GRANT,
восстановление удалённого plaintext без нового согласия и удаление канала.
При startup/restart дополнительно проверяются оба дерева: stale temp удаляются,
открытый и закрытый активные temp, конечные файлы и чужие имена сохраняются.
+192 -7
View File
@@ -3,6 +3,188 @@ ETCP: независимая обратная связь каналов и до
Статус
24.10.2026: исправлены абсолютные сроки ACK и конкуренция ACK/DATA разных путей.
Обычный ACK-only обслуживается от received_at_us головы своего пути + 2 мс;
piggyback готового DATA и срочный ответ остаются немедленными. Среди готовых ACK
выбирается самый старый срок; DATA другого пути уступает ему, DATA того же пути
забирает его piggyback. Ограниченная измерительная серия заканчивается до смены пути.
Один прежний таймер пересчитывается после piggyback/частичной отправки; новый RX
не отодвигает старые записи после callback таймера. Логический till имеет собственный
абсолютный срок вместо отдельного boolean pending. При OOM таймера используется
существующий флаг срочного ACK, исходные received_at_us не меняются.
Новых очередей/таймеров нет; wire-формат, RTO, min_rtt и модель BBR сохранены.
Две регрессии до фикса воспроизводят ACK в возрасте 0 мкс и обход готового ACK
другого пути исходящим DATA: /tmp/etcp-ack-deadline-before.log,
/tmp/etcp-ack-due-priority-before.log. Новые проверки покрывают границы 1999/2000 мкс,
piggyback разных путей, смену головы, early timer callback, RX между timer/sender,
частичный ACK-кадр, срочный ответ, OOM, sender failure с close, reset и DOWN/UP/удаление.
Диагностика расширена до 106 полей: ранние записи внутри готовой пачки допустимы;
early_ordinary_ack_packets отдельно проверяет отправку всего пакета раньше срока.
Проверка после исправления: windows 60 сценариев PASS с управляемыми и реальными
часами; восемь профильных тестов ETCP/STCP ASAN/UBSan с detect_leaks=1 PASS без
ошибок/утечек. Отдельный ASAN two-stuck с диагностикой 10 мс: 5202 проверенных среза,
ранних ACK-пакетов нет, доставка/ресурсы PASS. Полный check.sh: 184/184, proxy 9/9,
burst 1/1, load 8/8 PASS, 134217728 проверенных байт, 185 с со сборкой.
Три последовательных bidirectional с диагностикой: ранних обычных ACK-пакетов 0
во всех трёх прогонах. На одиночном 8 Мбит/с пути полоса ACK 0.143..0.169 Мбит/с
вместо 0.347/0.390 в предыдущем диагностическом прогоне. Агрегат: A->B
8.667..8.804, B->A 8.695..8.796 из 10 Мбит/с. Сам агрегат проходит все свои пороги
каждый раз; весь профиль остаётся FAIL из-за одиночных каналов, с 3/2/4 нарушениями.
Средний возраст entries ACK-only на 8 Мбит/с может быть меньше 2 мс: внутри готового
кадра передаются и новые entries. Это учитывается отдельно от ранней отправки кадра.
Полная штатная последовательная матрица после калибровки эмулятора: первые шесть
скоростных профилей и все 9 профилей отказов PASS. Агрегированный goodput,
Мбит/с: equal 14.416/16, bandwidth 9.032/10, rtt 14.516/16, fast-distant 8.857/10,
slow-distant 8.978/10, three 11.122/12, bidirectional 8.750/8.768 из 10 на направление.
Остаются два FAIL одиночного дуплекса: 1.609 вместо 1.700 Мбит/с (путь 2 Мбит/с)
и 6.730 вместо 6.800 Мбит/с (обратное направление пути 8 Мбит/с).
Two-stuck: gap 206.227 мс, latency 375.457 мс; load-drop/load-hold: gap 41.402/41.845 мс,
steady goodput 7.229/7.466 из 8 Мбит/с. Max latency насыщенного источника
180.693/89.239 мс записана; отдельным порогом она здесь не ограничена.
Дальше отдельно разбирать недооценку полосы/PROBE_RTT одиночного дуплекса,
STCP backpressure и проверки изменения RTT/BW и повторного отказа.
Логи: /tmp/etcp-ack-deadline-{windows,real-time,full-check}.log,
/tmp/etcp-ack-deadline-asan-test_*.log, /tmp/etcp-ack-deadline-asan-two-stuck.log/.csv,
/tmp/etcp-ack-deadline-matrix.log/.csv, /tmp/etcp-ack-deadline-duplex-<1..3>.log/.csv.
Сводка ACK: /tmp/etcp-ack-deadline-summary.json; актуальный HTML с 106 полями:
/tmp/etcp-ack-deadline-duplex.html. Проверка CSV и JS smoke графиков/фильтров PASS.
Предыдущий этап: диагностика до исправления ACK.
24.10.2026: добавлена буферизованная диагностика multipath с шагом 100 мс.
Интервал задаётся UTUN_MULTIPATH_DIAG_MS=10..1000. Новых таймеров нет: срез снимается
существующим тестовым циклом, фактический dt_us записывается в каждой строке.
Конец фазы добавляет короткий срез, поэтому скорость считают по dt_us, а не всегда по 100 мс.
Данные сохраняются в память и выводятся в CSV/лог после drain каждого стенда;
при провале CHECK уже собранные срезы выводятся перед exit. Переполнение буфера — ошибка.
Этот этап меняет только диагностику теста, алгоритмы ETCP/BBR сохранены.
Запуск из корня репозитория:
UTUN_MULTIPATH_DIAG=/tmp/etcp-duplex.csv ./tests/test_etcp_multipath bidirectional > /tmp/etcp-duplex.log 2>&1
python3 tools/etcp_diag_report.py /tmp/etcp-duplex.csv /tmp/etcp-duplex.html
Для подробного отказа: UTUN_MULTIPATH_DIAG_MS=10 и профиль two-stuck.
Уровень категории debug явно включается самим тестом; без UTUN_MULTIPATH_DIAG новый вывод выключен.
104 поля связывают очередь приложения, partial/pending/ожидания normalizer, очередь
фрагментов, send_q (новые/повторы), wait_ack и recv_q с физическим flight/cwnd пути.
Видны suspended/resume/deferred callbacks, состояние sender, номера seq/till,
общий бюджет DATA, очередь ACK и возраст её головы, возраст старейшей попытки,
RTT/jitter/RTO, BW, режим/фаза BBR, pacing, долг и срок готовности pacer, флаг застревания.
Отдельно записываются DATA TX/первые DATA RX, ACK-only/ACK в DATA, задержки ACK,
обычные ACK-only раньше 2 мс, samples BBR, проверки блокировки окном и pacer,
наблюдаемые min/max flight/cwnd/долга и пики send_q/ACK внутри интервала.
Очереди соединения повторяются в строках его линков — их нельзя суммировать между путями.
У pacer положительный долг допускается строго меньше 1 мс; отрицательное значение —
сырой кредит, который при отправке ограничивается 1 мс. Блокировки выбора — число
проверок, а не длительность ожидания. Пики внутри среза — наблюдения на TX/RX/ACK/выборе,
не непрерывная запись каждого изменения. channel_pending включает propagation delay,
сериализацию и HOLD; channel_peak/delay_max накапливаются с последнего reset_stats.
Wire-байты — UDP без IP/UDP headers; ACK-only включает timestamp, crypto и SESSION.
Piggyback учитывает только положительную ACK-секцию, без повторного начисления crypto.
HTML автономен, без библиотек/сети: выбор стенда/направления/канала/фазы/интервала,
восемь графиков и все поля выбранного курсором среза. PROBE_RTT выделен фоном.
Перед построением проверяются схема CSV, учёт очередей, счётчиков и непрерывность интервалов.
Проверены 598 срезов дуплекса (100 мс), 5208 срезов two-stuck (10 мс) и 5196 срезов
ASAN/UBSan two-stuck с detect_leaks=1 без ошибок/утечек. Намеренный CHECK сохранил
72 среза до выхода. Неверные интервалы 6/6 и повреждённые CSV 3/3 отклонены.
Общий check.sh итоговой версии: 184/184, proxy 9/9, burst 1/1, load 8/8 PASS,
134217728 проверенных байт, 166 с. HTML прошёл JS smoke-проверку восьми графиков,
104 полей, фильтров, диапазона, курсора, пустого диапазона и сброса.
Длинный дуплекс отдельно по-прежнему FAIL по скорости: в последнем прогоне 5 нарушений,
агрегат 8.513/8.337 из 10 Мбит/с. Это результат одного прогона, не доказательство эффекта диагностики на скорость.
На одиночном дуплексном пути 8 Мбит/с итоговые срезы показывают расход ACK 0.347/0.390 Мбит/с.
Обычные ACK-only раньше 2 мс: 1774/1828 и 1961/2019 записей; среднее ожидание 230/271 мкс.
Наблюдаемый пик очереди ACK — 4 записи, включая короткие пики до немедленной отправки.
У side=1 path=1 mask=2 в 4.5..5.4 с есть 4 кодограммы приложения, фрагмент и до 8 DATA
в send_q, а pacing составляет около 6.24..6.26 Мбит/с. В 5.5..5.7 с PROBE_RTT уменьшает
окно примерно с 81.5 до 32.9 тыс. байт; после него pacing возвращается лишь к 6.27..6.35 Мбит/с.
В этих срезах виден заданный моделью темп, а не отсутствие данных у sender.
Обнаружен обход ACK_DELAY посторонним wake sender. Диагностическое отключение PROBE_RTT
повышает скорость одиночного 8 Мбит/с пути примерно с 6.8 до 7.1 Мбит/с, но не закрывает
весь дуплекс. Прототип ожидания общего таймера уменьшает полосу ACK примерно до 0.21 Мбит/с,
однако также не закрывает все пороги. Эти прототипы остаются в /tmp, в протокол не внесены.
Следующий разбор: абсолютные deadlines ACK и переустановка единственного таймера после
piggyback/частичной отправки. Проверка одного наличия timer недостаточна: новый RX между
срабатыванием timer и отложенным sender может повторно задержать уже готовые старые ACK.
Артефакты: /tmp/etcp-duplex-diag-{100ms,10ms,asan,abort}.csv/.log,
/tmp/etcp-duplex-diag-view.html, /tmp/etcp-fault-diag-view.html,
/tmp/etcp-duplex-diag-full-check.log. Полный пакетный трейс и контроль PROBE_RTT:
/tmp/etcp-duplex-next-{trace,no-probe-control,ack-control}.log,
/tmp/etcp-duplex-next-analysis.json; observer — /tmp/etcp-probe-review.c/.mk.
Сводка итоговых срезов: /tmp/etcp-duplex-diag-summary.json.
24.10.2026: реализованы сканирование send_q, история путей пакета и быстрое застревание.
Остались прежние input_send_q/input_wait_ack: первый готовый повтор имеет приоритет,
иначе выбирается первый готовый новый DATA. Заблокированная запись остаётся на месте
и не мешает следующим. Если кандидата нет, допускается одна подгрузка из input_queue
в пределах общего бюджета unacked_bytes/optimal_inflight и приёмного окна пира.
После синхронного callback проверяются reset/close/эпоха; прежний указатель линка не используется.
Во время ограниченной burst-серии готовый повтор другого пути ждёт её завершения;
FILLER сохраняет измерение, а новые DATA не обгоняют этот повтор.
Каждый логический пакет хранит 32 байта битовой истории живых слотов local_link_id.
Пока есть неиспользованный допустимый путь, повтор ждёт его cwnd/pacer, а не начинает
новый обход уже использованных. После полного обхода прежний путь также уступает
другой допустимой альтернативе. История/last_rr_link меняются только после принятия
DATA транспортом; rollback их сохраняет. Удаление линка очищает слот до unlink и
callbacks, поэтому новый линк с тем же local_link_id не наследует старую историю.
Три RTO физических попыток без ACK-прогресса ставят tx_stalled. Пока есть хотя бы
один незастрявший initialized+UP путь, повторы исключают застрявшие, даже когда
рабочий временно занят. Если застряли все — разрешены все UP-пути. Новые DATA
используют прежний выбор и могут проверить восстановление: флаг снимает действующий
ACK собственной попытки, отправленной после установки флага. Старый/чужой ACK
этого не делает. Reset очищает состояние; физический UP, ACK/keepalive и формула RTO
сохранены. Новых очередей, payload-копий, таймеров или ранних повторов до RTO нет.
Windows: 51 сценарий PASS с управляемыми и реальными часами. Покрыты заблокированный
префикс 256 записей, приоритет повторов, общий бюджет, OOM, rollback, полный обход,
ID 255/переиспользование слота из callback, три RTO, старые ACK, восстановление,
все застрявшие пути и reset/close/удаление burst-линка из callback подгрузки.
Восемь профильных тестов ASAN/UBSan с detect_leaks=1 проходят без ошибок/утечек.
Контроль занятости рабочего пути 1 мс теперь проходит: latency 377.012 мс вместо 736.623 мс,
gap 206.810 мс, ready_wait 2.318 мс при бюджете 460.552 мс. Отрицательный контроль 200 мс
по-прежнему проваливает ready_wait (200.764 мс), gap и latency; пороги не ослаблены.
Логи: /tmp/etcp-send-scan-{windows-final,real-time,busy_1ms,ready_delay_200ms}.log,
/tmp/etcp-send-scan-asan-test_*.log.
Полная последовательная матрица после калибровки эмулятора: все 9 профилей отказов PASS.
Two-stuck: gap 217.945 мс, latency 379.997 мс при пределе 462.219 мс, ready_wait 1.184 мс.
Ещё три отдельных повтора two-stuck: 3/3 PASS, latency 374.981..378.557 мс,
gap 203.057..205.265 мс, ready_wait максимум 13.268 мс. Slow-drop: gap 259.464 мс,
latency 264.704 мс при пределе 350.129 мс; это профиль с RTT 200 мс и ожиданием RTO.
Load-drop/load-hold: gap 39.234/39.355 мс, steady goodput 7.221/7.468 из 8 Мбит/с.
Для насыщенного источника проверяются gap и steady goodput; max_latency 161.287/86.306 мс
здесь записана, но отдельным порогом не ограничена. ASAN/UBSan two-stuck/load-drop
также PASS с detect_leaks=1, без ошибок и утечек.
Первые шесть скоростных профилей PASS. Агрегированный goodput
(min(application,fresh DATA), Мбит/с): equal 14.390/16, bandwidth 9.048/10,
rtt 14.244/16, fast-distant 8.841/10, slow-distant 8.966/10, three 11.017/12.
Дуплекс 8.412/8.435 из 10 Мбит/с остаётся FAIL: все 7 нарушений матрицы относятся
к его скоростным порогам (одиночные каналы и агрегат). Доставка, порядок и ресурсы
проходят. Общая multipath-задача остаётся открытой: далее разбирать дуплекс,
STCP backpressure и отдельные проверки смены RTT/BW/повторного отказа.
Логи: /tmp/etcp-send-scan-matrix.log/.csv,
/tmp/etcp-send-scan-native-two-stuck-<1..3>.log/.csv,
/tmp/etcp-send-scan-asan-{two-stuck,load-drop}.log/.csv.
Полный check.sh после изменений: 184/184, proxy 9/9, burst 1/1, load 8/8 PASS;
134217728 проверенных байт, 197 с со сборкой. Отдельная длинная multipath-матрица
в check.sh не входит; её оставшиеся нарушения дуплекса перечислены выше.
Лог: /tmp/etcp-send-scan-full-check.log.
Предыдущая диагностика (до реализации сканирования и истории):
24.10.2026: проверки отказов разделены на RTO, готовность повтора и доставку.
В этом этапе менялись только тесты/диагностика, логика ETCP сохранена.
TX учитывается после принятия send транспортом, а не при подготовке пакета.
@@ -714,11 +896,12 @@ uint8: хранить факт состоявшейся отправки отд
Для retry: A->B при обоих UP и меньшем inflight у A; A->B->C->A;
посторонние DATA/ACK между попытками; пропуск заблокированных каналов;
один доступный путь; все временно заняты; burst старого пути; удаление курсора.
После этого повторить всю матрицу скоростей. Выбор пути первоначальных DATA менять только
Матрица повторена после сканирования send_q и истории путей; текущие результаты приведены в статусе.
Выбор пути первоначальных DATA менять только
при сохраняющейся недогрузке: эксперименты уже показывают полную утилизацию
без смены алгоритма выбора DATA, поэтому дополнительная эвристика пока лишняя.
3. Быстрое восстановление и здоровье каналов.
3. Быстрое восстановление и здоровье каналов — согласованный этап реализован.
Предварительное исправление OOM завершено: выбран сброс всей сессии с сохранением
линков, без удержания старого fragment/cursor и без отдельного OOM retry.
Очистка не выделяет память, старые callbacks/ожидания отменяются; активные
@@ -727,11 +910,13 @@ uint8: хранить факт состоявшейся отправки отд
Собственный RTT/RTO и ротация уже обязательны в этапе 2. Проверять ожидание
RTO отдельно от задержки готового повтора и доставки альтернативным путём.
Ранние копии до RTO исключены по решению пользователя от24.10.2026.
При потере ограничить новые DATA на проблемном пути, пробовать восстановление
ограниченными probe, повторять недоставленные DATA через рабочие каналы.
Не отправлять следующий bulk вслепую в тот же застрявший путь.
Проверки: DROP/HOLD отдельно по направлениям и вместе, несколько застрявших
каналов, возврат старых копий после HOLD, повторный отказ, смена RTT/BW на ходу.
Реализованы флаг после трёх RTO, исключение застрявшего пути только из повторов,
полная история обхода, сканирование send_q и восстановление по свежему собственному ACK.
Отдельные probes/ограничение новых DATA не входят в выбранное решение: исходный
выбор новых DATA сохранён, подтверждённая новая отправка снимает флаг застревания.
DROP/HOLD отдельно по направлениям и вместе, несколько застрявших каналов,
возврат старых копий после HOLD проходят; повторный отказ и смена RTT/BW на ходу
остаются отдельными проверками. Отсутствие рабочих альтернатив и reset/close покрыты.
4. Учёт нижележащей очереди STCP.
Ограниченный tx_queue и уведомление о реальном прогрессе/готовности должны
+17 -2
View File
@@ -26,8 +26,9 @@ HELLO/CHALLENGE/CONFIRM и keepalive передаются своим незав
При наличии незастрявшего initialized+UP пути застрявшие исключены из повторов, даже если альтернатива занята.
Если застряли все, доступны все UP-пути. Прежний путь разрешён лишь при отсутствии других допустимых;
иначе ждём альтернативу. DOWN последней альтернативы пробуждает отправителя.
3. Если DATA не помещается, он остаётся в очереди; для накопленных ACK выбирается
тот же линк, который принял соответствующий DATA, без ограничения DATA-cwnd/pacing.
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 из очереди и изменения счётчиков.
@@ -61,6 +62,20 @@ HELLO/CHALLENGE/CONFIRM и keepalive передаются своим незав
Если логический 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
+211
View File
@@ -0,0 +1,211 @@
Аудит media_async / media_delivery — 2026-10-24
Статус: подтверждённый дефект исправлен; полный аудит двух каталогов ещё не завершён.
Ниже сохранены исходное воспроизведение и согласованный проект; результаты исправления — в конце.
Подтверждённый дефект: исходник уничтожается через другое имя того же файла
encrypted_file_write, src/media_async/encrypted_file.c:79-87, сравнивает только строки
source/destination. Разные строки могут обозначать один inode. fopen(destination,"wb")
усекает уже открытый source. Криптография затем успешно обрабатывает пустой файл.
Контейнер корректен для нулевого содержимого, но исходные данные потеряны; возврат равен 0.
Воспроизведено на настоящих функциях из src/libutun.a:
- source и source с компонентом /./: исходные30 байт заменены контейнером152 байта,
plaintext_size=0, rc=0, SHA256 исходника изменился;
- source и hardlink: тот же результат;
- encrypted_file_read тоже пропускает оба вида alias: контейнер182 байта заменяется
plaintext30 байт, rc=0, исходный ciphertext больше не существует в прежнем виде.
У read такая же проверка strcmp в строке118 и разрушающий fopen в строке142.
В текущем групповом pipeline вызывающий выбирает отдельный .part; воспроизведение
доказывает дефект файлового API, а не наблюдавшуюся потерю реального пользовательского вложения.
Ошибку нельзя замаскировать описанием одинаковых файлов как успешного no-op:
шифрование и расшифровка должны создавать другое содержимое.
Диагностика:
- /tmp/media-encrypted-alias-repro.c — четыре проверки, сверяются inode, размеры и SHA256;
- /tmp/media-encrypted-alias-repro.mk — использует цели/библиотеки/флаги tests/Makefile;
- /tmp/media-encrypted-alias-repro.log — полный журнал с media=debug;
- /tmp/media-encrypted-alias-lvVS77/ — сохранённые входы и результаты опыта.
Проектные .c/.h для воспроизведения не менялись.
Согласованная архитектура исправления
Одной проверки file_io_paths перед fopen(wb) недостаточно для полного исправления
контракта владения. Сейчас encrypted_file удаляет destination при ошибке, а
ak_file_done повторно удаляет заранее вычисленный .part даже при раннем отказе.
Нужно убрать открытие произвольного назначения с усечением и очистку по одному имени.
Граница модулей
- lib/file_io: UTF-8/regular-file проверки, идентичность, эксклюзивный temp,
flush/sync/close, atomic rename, владение temp и защита от orphan cleanup.
- encrypted_file: создание/проверка контейнера в собственном temp; без публикации
конечного имени, SQLite и состояния async-задачи.
- chat_attachment: задача владеет temp, выбирает конечное имя и выполняет одну
публикацию; completion меняет состояние БД/UI и освобождает только свои ресурсы.
Подготовка, sync и rename остаются в worker, как сейчас; новые файловые I/O в uasync не добавляются.
Предлагаемый контракт write/read
1. Функция получает source, конечный target_path и указатель на обнулённый file_io_temp.
target_path задаёт место будущего результата; он не открывается с усечением.
До начала работы file_io_paths(source,target_path) обязан вернуть 0.
Совпадение строки, /./, hardlink и конечная ссылка назначения отклоняются без изменений.
2. Источник открывается общим file_io_open_read. Только после успешного открытия
создаётся уникальный temp рядом с target_path через file_io_temp_open.
На POSIX он имеет права 0600; создание эксклюзивное, владелец сразу учтён в общем реестре.
3. Контейнер пишется/проверяется через уже открытый FILE этого temp. Нет второго .part,
второго открытия выхода по пути или дополнительного слоя публикации внутри codec.
4. Полностью проверяются данные, закрытие входа и durable завершение выхода.
При ошибке удаляется только созданный этой операцией temp; существующие source/target сохраняются.
При успехе возвращается закрытый, синхронизированный temp, который ещё не опубликован.
Результат info/type у read присваивается только после полного успешного завершения подготовки.
5. Владелец file_io_temp размещается прямо в ak_file и имеет стабильный адрес до done;
копировать его структуру нельзя: реестр хранит адрес владельца.
Функции не принимают уже активный output и не уничтожают его при отказе аргументов.
Единая политика durability
В file_io_temp_open добавить flags, в file_io_temp хранить выбранный режим на весь
срок операции. FILE_IO_DURABLE включает обе части: finish и publish.
Обычные пользователи передают 0 и сохраняют свою семантику; отдельные флаги в каждом
finish/publish не нужны. Это предотвращает случайное сохранение файла без сохранения имени.
Durable finish: fflush -> fsync/_commit того же открытого fd -> fclose. Ошибка любой
фазы сохраняет первый код ошибки; close выполняется и при отказе sync. Открывать путь
заново ради fsync не нужно. После отказа temp остаётся у владельца до discard.
Publish повторно проверяет идентичность source/target и принимает только закрытый temp.
На POSIX: rename и fsync каталога назначения; соседний temp находится в том же каталоге.
На Windows: общий wide API и MOVEFILE_WRITE_THROUGH. Родительский каталог вычисляется
без обрезания: имя без '/' означает '.', '/file' означает '/'.
Политика sync принадлежит file_io, а не формату зашифрованного контейнера.
Точка фиксации — успешный rename. До неё ошибка сохраняет старый target.
После rename ошибка fsync каталога означает: результат уже установлен, долговечность
имени не подтверждена. Нельзя обещать откат, удалять новый файл вслепую или восстанавливать
старое содержимое. В этот момент temp уже отсоединён от реестра и его имя в owner пусто.
Этот переход должен быть явно описан в file_io.h и проверен тестом.
Длительный fsync выполняется после снятия блокировки реестра временных файлов.
Владение задачи и отмена
ak_file хранит file_io_temp вместо temporary[...]. Worker после успешной подготовки
один раз вызывает file_io_temp_publish с тем же target и источником.
До publish поле temporary непустое; после успешного rename оно пустое даже при
ошибке directory sync. По этому переходу задача фиксирует факт своей публикации.
Completion всегда может вызвать discard: после rename он ничего не удаляет.
Cleanup при отмене/удалении канала учитывает отдельно действительно созданные/
заменённые артефакты. Вычисленный путь не является доказательством владения файлом.
При отказе до создания выхода нельзя удалять существующий target или его alias.
Отдельных async owners, refcount и фоновых потоков не требуется. Ключ по-прежнему сохраняется
до шифрования; ready получателя/публикация сообщения подтверждаются только после
успешного worker и проверки актуальности канала в completion. Авторская строка ключа
сохраняется до worker по прежним правилам. Общей транзакции ФС/SQLite нет: после rename
путь остаётся известен attachment_keys для последующей очистки/повторной операции.
Кеш и восстановление после crash
Активный temp защищён существующим file_io-реестром, в том числе после finish.
В chat_msg убрать специальное сохранение вычисленного cipher_path.part; конечный
cipher_path по-прежнему защищается attachment_keys, временный файл — своим owner.
После crash реестра нет: такие temp допустимо удалить.
Нельзя ограничиться нынешним orphan walk дерева media: plaintext-temp находится
в decrypted, а ak_recover_preparations знает только фиксированный .part.
В init/очистке после завершения workers нужно удалять оставшиеся временные файлы
строгого собственного формата <UUID>.uenc.tmp.<32hex> / <UUID>.file.tmp.<32hex>
в управляемых каталогах media/decrypted. Использовать file_io_remove_orphan,
чтобы сохранить активные temp другого instance того же процесса. Конечные файлы,
ключи и данные иных форматов такой проход не удаляет; ошибка обхода логируется.
Область изменений
Основные файлы: lib/file_io.c/h, media_async/encrypted_file.c/h,
chat/chat_attachment.c, chat/chat_msg.c, test_encrypted_file/test_media_primitives
и сетевые проверки зашифрованных вложений. Остальные вызовы file_io_temp_open
в media_async/media_delivery/video переводятся на явные flags=0.
DM не вызывает encrypted_file_write/read: он использует media_crypto streams
и общие sync/publish helper-функции из encrypted_file.h. При правке общего слоя
необходимо сохранить их durable контракт; перенос общих операций в file_io должен
быть прямым, без совместимых обёрток и без изменения DM state machine.
Формат контейнера, nonce, протокол ключей и схема БД для этого фикса не меняются.
Регрессии и критерии готовности
- Обе операции: одинаковая строка, /./, hardlink, symlink, отсутствующий/необычный source;
source и прежний target не меняются, alias не удаляется, чужой temp не освобождается.
- Обычные файлы размером 0/1/32767/32768/32769 байт, большой файл, UTF-8 пути;
успешная подготовка не меняет target до publish. Read не публикует info/type при ошибке.
- Внедрение ошибок open/fdopen/read/write/seek/flush/fsync/input-close/output-close/
rename/directory-sync/RNG; отсутствие утечек fd/памяти/регистраций temp.
- Подмена target на hardlink между подготовкой и publish: повторная проверка
отклоняет публикацию, исходник сохраняется, удаляется только собственный temp.
- Отказ directory sync после rename: новый файл цел, temp-owner отсоединён,
ошибка не представляется как откат; повторная операция корректна.
- Orphan cleanup во время work, после finish, после publish; crash/restart
в обоих деревьях, чужие имена сохраняются. Отмена, stop и удаление канала
до work, после подготовки и после rename: один done, нет публикации сообщения/ready.
- Профильные 20 тестов, ASAN/UBSan/LSan и полный набор. Изменение file_io требует
также проверки его прежних пользователей: copy, video и media blocks/download.
Этот проект реализован. Успешные 20 тестов ниже относятся к исходной реализации;
проверки исправленного кода приведены в конце.
Что проверено и что осталось
Прочитаны все шесть пар .c/.h media_async, целиком file_transfer.c/h, заголовки
media_delivery/download/index/proto, sync.md и doc/encrypted_attachments.md.
Полная повторная сверка остальных реализаций media_delivery и исправление контрактов
ещё не выполнены. Прежние [V] в общем checklist относятся к предыдущим аудитам;
новые незавершённые пункты отмечены [~].
Linux: ./build.sh --no-gui -j4 — PASS;20/20 профильных тестов — PASS:
media_primitives, attachment, attachment_send, encrypted_file, encrypted_attachment,
media_async, media_index, media_index_regressions, media_delivery_sql,
media_delivery_download, media_download_timeout, media_query, media_blocks,
media_replication, media_delivery_integration, media_delivery_full,
media_delivery_chat, dm_media, dm_worker, services.
Логи: /tmp/media-audit-build.log, /tmp/media-doc-audit-results.log,
/tmp/media-doc-audit-logs/. Штатный test_encrypted_file не покрывает alias.
Старые сбои encrypted_attachment/media_delivery_chat в этом прогоне не воспроизвелись;
один PASS не закрывает задачи о нестабильности.
На исходном этапе полный check.sh и новые sanitizer-прогоны не запускались.
Результаты исправления — 2026-10-24
- file_io_temp получил единый FILE_IO_DURABLE: sync открытого fd при finish,
write-through rename на Windows и directory sync после rename на POSIX.
После rename temporary пуст, включая отказ directory sync; discard не удаляет результат.
Общие sync/publish функции перенесены в file_io; DM вызывает их напрямую.
- Write/read готовят отдельный закрытый temp. Source/target не открываются с усечением;
совпадающая строка, /./, hardlink и конечная ссылка отклоняются до создания выхода.
Read выдаёт info/type только после успешного закрытия/сохранения проверенного результата.
- ak_file хранит один output, учитывает подготовленный plaintext snapshot и выполненный rename.
Worker публикует один раз; completion очищает только собственные артефакты.
Кеш использует реестр владельцев; recovery очищает строгие temp в media и decrypted.
Построение путей и SQL recovery используют одну каноническую media_base.
- test_encrypted_file: прежние шесть размеров, восемь alias-сценариев обоих API,
активный output, alias перед publish и 23 отказа I/O с контролем fd/temp/метаданных.
До rename прежний target сохраняется; после отказа directory sync новый файл установлен.
- test_media_primitives: durable и обычный режим, sync failure до rename, failure после
rename, относительный/корневой каталог, защита активного temp в worker и после close.
- test_attachment_send детерминированно ждёт настоящего crypto rename без обработки
completion, затем останавливает сервис: нет сообщения/ключа/оставшихся файлов/temp.
- test_encrypted_attachment при каждом startup/restart проверяет оба дерева:
stale temp удалены; открытый/закрытый активные owners, конечные файлы и чужие имена сохранены.
- Профильный Linux-набор: 21/21 PASS, включая video/no_ffmpeg, DM, services и media delivery.
Основной make check: 185/185 PASS. TUN в отдельном network namespace с тем же бинарником:
proxy9/9, burst1/1, load8/8 PASS; 134217728 проверенных байт.
- ASAN/UBSan/LSan: 21/21 PASS с detect_leaks=1. Все встроенные зависимости собраны
с sanitizer-флагами в /tmp/utun-media-fix-sanitizers; обычные объекты проекта не смешивались.
Windows/FreeBSD build/runtime не проверялись.
Логи: /tmp/media-fix-profile-results.log, /tmp/media-fix-full-check.log,
/tmp/media-fix-network-check.log, /tmp/media-fix-sanitizer-results.log,
/tmp/media-fix-cancel-after-rename.log, /tmp/media-fix-encrypted-file.log.
Проверка реализации не выявила противоречий владения в перечисленных сценариях.
Частичные отметки остальных файлов аудита сохраняются; этот фикс не означает полного
завершения проверки media_async/media_delivery или закрытия старых нестабильных тестов.
+9
View File
@@ -136,6 +136,15 @@ completion. Отказ запуска/неверные аргументы даю
закрытия temp; вызывающий отвечает за очистку своего результата при отмене.
Подробные файловые контракты, 64-битные размеры и ошибки RNG описаны в `media_async.h`.
Групповой crypto worker хранит один `file_io_temp` прямо в задаче вложения.
`encrypted_file_write/read` только готовят закрытый durable temp; worker публикует
его одним rename, completion подтверждает результат в БД/UI. Отмена до completion
не публикует сообщение; cleanup учитывает реально созданные/заменённые файлы.
Ошибка синхронизации каталога после rename означает установленное имя без подтверждённой
durability; отложенный discard уже не удаляет результат. Общей транзакции ФС/БД нет.
До запуска workers и после их завершения recovery очищает собственные stale temp
в `media/decrypted`; общий file_io-реестр сохраняет активные temp всех instance.
`media_delivery_init()` вызывается после подготовки chat core и media_index, пока
общая БД и группы живы; подписка на роли обязательна. Репликация хранит отдельный
контекст и CM handle для каждой пары «CHAT-группа–пир». DOWN отменяет текущий RPC,
+62
View File
@@ -4,6 +4,24 @@
## Текущие задачи
[+] **media_async encrypted_file: не уничтожать source через alias назначения** — исправлено 24.10.2026.
write/read отвергают только одинаковые строки; /./ и hardlink обходят проверку одного файла.
write возвращает0, заменяя исходные30 байт контейнером пустого файла; read возвращает0,
заменяя входной ciphertext182 байта plaintext30 байт. Проверены реальные функции, inode и SHA256.
Реализован один file_io_temp в ak_file; codec готовит закрытый проверенный temp,
worker публикует его одним rename. Target не открывается с усечением; alias проверяется до work и перед publish.
Durability задаётся на весь срок temp; ошибка directory sync после rename не считается откатом.
Cleanup удаляет только собственные артефакты; crash recovery учитывает temp в media и decrypted.
DM не вызывает codec: сохранить его контракт общих sync/publish helpers при изменениях file_io.
Детали, проверка архитектуры исправления и незавершённый аудит: [media_async_audit_findings.txt](media_async_audit_findings.txt).
Воспроизведение: /tmp/media-encrypted-alias-repro.c, /tmp/media-encrypted-alias-repro.log.
Добавлены восемь alias-сценариев, 23 отказа I/O с контролем fd/temp/метаданных,
отмена настоящего worker после rename до completion, startup/restart обоих деревьев.
Профильные21/21 и основной make check185/185 PASS; proxy9/9, burst1/1, load8/8 PASS
в отдельном network namespace,134217728 проверенных байт. ASAN/UBSan/LSan21/21 PASS, detect_leaks=1.
Логи: /tmp/media-fix-full-check.log, /tmp/media-fix-network-check.log, /tmp/media-fix-sanitizer-results.log.
Полный аудит остальных контрактов media_async/media_delivery ещё не завершён; Windows/FreeBSD не проверялись.
[+] **Proxy: исправить HUP/backpressure, владение и rollback** — завершено 24.10.2026.
Согласованный проект, причины и результаты: [doc/proxy_fix_plan.txt](proxy_fix_plan.txt).
HUP больше не обходит read_high_water/ручную паузу; idle HUP снимается с poll,
@@ -25,6 +43,50 @@
Windows/FreeBSD runtime не проверялся.
[ ] **ETCP: полезная утилизация нескольких каналов и доставка при зависании части линков** — общая задача открыта.
24.10.2026: исправлен обход срока ACK посторонним wake. Абсолютный срок головы пути +2 мс,
один общий таймер, пересчёт после piggyback/частичной отправки; новые DATA не отодвигают старые ACK.
Наступивший ACK другого пути имеет приоритет перед DATA; свой DATA забирает его piggyback.
Сохранены срочный ответ, bounded burst, OOM fallback и очистка при reset/close; wire/BBR/RTO не менялись.
Windows 60 сценариев с обоими режимами часов PASS; восемь ASAN/UBSan-тестов и ASAN two-stuck без ошибок/утечек.
В трёх дуплексных прогонах ранних ACK-пакетов 0; на 8 Мбит/с ACK занимают 0.143..0.169 вместо прежних 0.347/0.390 Мбит/с.
Агрегат проходит все свои пороги: 8.667..8.804 / 8.695..8.796 из 10 Мбит/с; одиночные каналы ещё дают FAIL.
Полная матрица: первые 6 скоростных профилей и все 9 профилей отказов PASS, агрегат дуплекса 8.750/8.768.
Остались 2 FAIL одиночного дуплекса: 1.609<1.700 и 6.730<6.800 Мбит/с; дальше разбирать BW/PROBE_RTT и STCP backpressure.
Load-drop/load-hold: паузы 41.402/41.845 мс, steady 7.229/7.466 из 8 Мбит/с; max latency 180.693/89.239 мс записана.
Check.sh 184/184, proxy 9/9, burst 1/1, load 8/8 PASS, 134217728 проверенных байт, 185 с.
Диагностика 106 полей различает раннюю запись в готовой пачке и отправку всего ACK-пакета раньше срока.
Логи/CSV: /tmp/etcp-ack-deadline-*; HTML: /tmp/etcp-ack-deadline-duplex.html; детали — doc/etcp_multipath_plan.txt.
Предыдущий этап — диагностика до исправления ACK:
Продолжение 24.10.2026: диагностика срезов 100 мс (настройка 10..1000 мс), 104 поля; CSV и лог буферизуются до drain.
Видны очереди, callbacks/deferred waiter, DATA/ACK, flight/cwnd, BBR/RTT/RTO, долг/готовность pacer и канал эмулятора.
tools/etcp_diag_report.py проверяет CSV и строит автономные интерактивные графики с исходными срезами.
Дуплекс: 598 срезов; two-stuck/ASAN: 5208/5196 срезов, без ошибок sanitizer/утечек; CHECK сохраняет накопленный лог.
Итоговый check.sh 184/184, proxy 9/9, burst 1/1, load 8/8 PASS, 166 с; JS smoke-проверка графиков/фильтров PASS.
На одиночном 8 Мбит/с пути ACK занимают 0.347/0.390 Мбит/с, среднее ожидание ACK-only 230/271 мкс вместо окна 2 мс.
В срезах side=1 path=1 DATA готовы, а BBR задаёт около 6.3 Мбит/с; PROBE_RTT временно уменьшает окно до 32.9 тыс. байт.
Дедлайн обходит посторонний wake sender. Дальше разбирать абсолютные deadlines ACK; простого !timer недостаточно.
Алгоритмы протокола на этом этапе не менялись; полный дуплекс всё ещё не проходит скоростные пороги (5 нарушений, 8.513/8.337).
Логи/CSV: /tmp/etcp-duplex-diag-*; просмотр: /tmp/etcp-duplex-diag-view.html; детали — doc/etcp_multipath_plan.txt.
24.10.2026: реализованы сканирование send_q с приоритетом готовых повторов и ограниченная подгрузка новых DATA.
Заблокированный пакет пропускается. История путей пакета (32 байта) исключает новый обход до перебора допустимых;
история/RR фиксируются после принятого send, удаление линка очищает слот до callbacks/повторной выдачи ID.
После 3 RTO без ACK-прогресса tx_stalled исключает путь из повторов при наличии незастрявшего UP-пути.
Все застрявшие пути допускаются, новые DATA сохраняют прежний выбор; действующий ACK отправки после флага снимает его.
Старый/чужой ACK не восстанавливает путь; reset очищает состояние. Новых очередей/таймеров/копий нет, RTO сохранён.
Windows: 51 сценарий PASS с управляемыми/реальными часами; восемь ASAN/UBSan-тестов без ошибок/утечек.
Контроль занятого на 1 мс рабочего пути: latency 377.012 вместо 736.623 мс, ready_wait 2.318 мс, профиль PASS.
Отрицательный контроль задержки готового повтора 200 мс остаётся FAIL по очереди и общей задержке.
Полная последовательная multipath-матрица: все 9 профилей отказов PASS, первые 6 скоростных профилей PASS.
Two-stuck: latency 379.997 мс при пределе 462.219 мс; ещё 3/3 повтора — 374.981..378.557 мс.
Load-drop/load-hold: паузы 39.234/39.355 мс, steady goodput 7.221/7.468 из 8 Мбит/с.
Max latency под насыщенной нагрузкой 161.287/86.306 мс записана; отдельным порогом она здесь не ограничена.
Two-stuck/load-drop также проходят ASAN/UBSan с detect_leaks=1 без ошибок/утечек.
Остаётся дуплекс: 8.412/8.435 из 10 Мбит/с, все 7 нарушений матрицы — его скоростные пороги.
Далее — дуплекс, STCP backpressure, отдельные проверки смены RTT/BW и повторного отказа.
Полный check.sh: 184/184, proxy 9/9, burst 1/1, load 8/8 PASS, 134217728 проверенных байт, 197 с со сборкой.
Длинная multipath-матрица запускается отдельно; её общий результат остаётся FAIL из-за дуплекса.
Логи: /tmp/etcp-send-scan-*.log; подробности — doc/etcp_multipath_plan.txt.
Предыдущая диагностика:
24.10.2026: тесты отдельно проверяют обработку RTO (опоздание<=20мс), ожидание готового повтора (<=100мс)
и доставку сверх заданной задержки канала (<=100мс). TX учитывается после принятия send.
Быстрые профили сохраняют общий предел100мс; длинные учитывают по одному RTO отказавших путей плюс100мс.
+89 -19
View File
@@ -135,11 +135,88 @@ int file_io_paths(const char *source, const char *destination) {
return !memcmp(input.identity, output.identity, sizeof(input.identity));
}
int file_io_temp_open(struct file_io_temp *output, const char *destination) {
/* Синхронизировать именно открытый файл; повторного открытия имени нет. */
static int file_io_sync_descriptor(FILE *file, const char *path) {
#ifdef _WIN32
int result = _commit(_fileno(file));
#else
int result = fsync(fileno(file));
#endif
if (result) {
int error = errno ? errno : EIO;
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file sync failed path=%s errno=%d", path, error);
errno = error;
}
return result ? -1 : 0;
}
int file_io_sync_file(const char *path) {
FILE *file = file_io_open_update(path);
if (!file) return -1;
int result = file_io_sync_descriptor(file, path);
if (fclose(file)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file sync close failed path=%s errno=%d", path, errno); result = -1;
}
return result;
}
int file_io_sync_directory(const char *path) {
if (!path || !*path || strlen(path) >= FILE_IO_PATH_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file directory sync invalid path"); return -1;
}
#ifndef _WIN32
char parent[FILE_IO_PATH_MAX];
memcpy(parent, path, strlen(path) + 1);
char *slash = strrchr(parent, '/');
if (!slash) memcpy(parent, ".", 2);
else if (slash == parent) slash[1] = 0;
else *slash = 0;
int fd = open(parent, O_RDONLY | O_DIRECTORY);
if (fd < 0) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file directory open failed path=%s errno=%d", parent, errno); return -1; }
int error = fsync(fd) ? (errno ? errno : EIO) : 0;
if (close(fd) && !error) error = errno ? errno : EIO;
if (error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file directory sync/close failed path=%s errno=%d", parent, error); return -1; }
#endif
return 0;
}
/* Только rename; владелец temp снимает регистрацию до длительного directory sync. */
static int file_io_rename(const char *source, const char *destination, unsigned flags) {
#ifdef _WIN32
wchar_t from[FILE_IO_PATH_MAX], to[FILE_IO_PATH_MAX];
if (file_io_wide(source, from) || file_io_wide(destination, to)) return -1;
DWORD options = MOVEFILE_REPLACE_EXISTING | ((flags & FILE_IO_DURABLE) ? MOVEFILE_WRITE_THROUGH : 0);
if (!MoveFileExW(from, to, options)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file rename failed source=%s destination=%s winerr=%lu", source, destination, GetLastError());
return -1;
}
#else
(void)flags;
if (rename(source, destination)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file rename failed source=%s destination=%s errno=%d", source, destination, errno); return -1;
}
#endif
return 0;
}
int file_io_publish(const char *source, const char *destination) {
if (file_io_paths(source, destination)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish invalid source/destination"); return -1;
}
if (file_io_rename(source, destination, FILE_IO_DURABLE)) return -1;
if (file_io_sync_directory(destination)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file installed but directory durability unconfirmed destination=%s", destination); return -1;
}
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file durably installed destination=%s", destination);
return 0;
}
int file_io_temp_open(struct file_io_temp *output, const char *destination, unsigned flags) {
if (!output || output->file || output->temporary[0] || !destination || !*destination
|| strlen(destination) + 38 >= FILE_IO_PATH_MAX) {
|| (flags & ~FILE_IO_DURABLE) || strlen(destination) + 38 >= FILE_IO_PATH_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary invalid owner/path"); return -1;
}
output->flags = flags;
for (unsigned attempt = 0; attempt < 8; attempt++) {
uint8_t random[16]; char suffix[33];
if (random_bytes(random, sizeof(random))) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary RNG failed"); return -1; }
@@ -171,7 +248,7 @@ int file_io_temp_open(struct file_io_temp *output, const char *destination) {
if (!output->file) close(fd);
#endif
if (!output->file) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file fdopen failed path=%s errno=%d", output->temporary, errno); return -1; }
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file temporary opened path=%s", output->temporary);
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file temporary opened path=%s durable=%u", output->temporary, flags & FILE_IO_DURABLE);
return 0;
}
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary collision limit destination=%s", destination); return -1;
@@ -180,9 +257,12 @@ int file_io_temp_open(struct file_io_temp *output, const char *destination) {
int file_io_temp_finish(struct file_io_temp *output) {
if (!output || !output->file) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file finish without open file"); return -1; }
int error = fflush(output->file) ? (errno ? errno : EIO) : 0;
if (!error && (output->flags & FILE_IO_DURABLE) && file_io_sync_descriptor(output->file, output->temporary))
error = errno ? errno : EIO;
if (fclose(output->file) && !error) error = errno ? errno : EIO;
output->file = NULL;
if (error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file flush/close failed path=%s errno=%d", output->temporary, error); return -1; }
if (error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file flush/sync/close failed path=%s errno=%d", output->temporary, error); return -1; }
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file temporary finished path=%s durable=%u", output->temporary, output->flags & FILE_IO_DURABLE);
return 0;
}
@@ -212,25 +292,15 @@ int file_io_temp_publish(struct file_io_temp *output, const char *source, const
if (!output || output->file || !output->temporary[0] || file_io_paths(source, destination)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish invalid state/paths"); return -1;
}
#ifdef _WIN32
wchar_t from[FILE_IO_PATH_MAX], to[FILE_IO_PATH_MAX];
if (file_io_wide(output->temporary, from) || file_io_wide(destination, to)) return -1;
TEMP_LOCK();
if (!MoveFileExW(from, to, MOVEFILE_REPLACE_EXISTING)) {
DWORD error = GetLastError(); TEMP_UNLOCK();
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s winerr=%lu", destination, error); return -1;
}
#else
TEMP_LOCK();
if (rename(output->temporary, destination)) {
int error = errno; TEMP_UNLOCK();
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s errno=%d", destination, error); return -1;
}
#endif
if (file_io_rename(output->temporary, destination, output->flags)) { TEMP_UNLOCK(); return -1; }
file_io_temp_untrack(output);
output->temporary[0] = 0;
TEMP_UNLOCK();
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file published destination=%s", destination);
if ((output->flags & FILE_IO_DURABLE) && file_io_sync_directory(destination)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary installed but directory durability unconfirmed destination=%s", destination); return -1;
}
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file published destination=%s durable=%u", destination, output->flags & FILE_IO_DURABLE);
return 0;
}
+16 -4
View File
@@ -4,7 +4,8 @@
* Активный temp автоматически защищён от remove_orphan во всех потоках до publish/discard, включая время после finish.
* Пока temp активен, структуру нельзя копировать/перемещать; с одним temp работает один владелец.
* Исходник и каталоги должны оставаться стабильными на время операции. Это не защита от подмены родительских каталогов.
* Publish заменяет имя атомарно, но не обещает сохранность при аварии питания. POSIX temp имеет права 0600. */
* FILE_IO_DURABLE сохраняет данные при finish и имя при publish. POSIX temp имеет права 0600.
* После успешного rename temp отсоединён даже при ошибке синхронизации каталога: отката публикации нет. */
#ifndef FILE_IO_H
#define FILE_IO_H
#include <stdio.h>
@@ -12,6 +13,7 @@
#include <stddef.h>
#define FILE_IO_PATH_MAX 4096
#define FILE_IO_DURABLE 1u
enum file_io_type { FILE_IO_REGULAR = 1, FILE_IO_DIRECTORY, FILE_IO_LINK, FILE_IO_OTHER };
struct file_io_info {
int type; // обычный файл, каталог, ссылка/reparse point или другой объект
@@ -22,6 +24,7 @@ struct file_io_temp {
char temporary[FILE_IO_PATH_MAX]; // только собственный temp; пусто после discard/publish
FILE *file; // открытый wb+; NULL после finish
struct file_io_temp *next_owned; // внутренний реестр живых temp; изменяет только file_io под блокировкой
unsigned flags; // режим, заданный open: 0 либо FILE_IO_DURABLE на весь срок операции
};
// path — UTF-8; follow!=0 разрешает конечную ссылку. info обязателен, заполняется только при успехе.
@@ -36,13 +39,22 @@ FILE *file_io_open_read(const char *path);
FILE *file_io_open_update(const char *path);
// Источник должен быть regular; назначение отсутствует или regular без конечной ссылки. 1 — один файл, 0 — разные, -1 — ошибка.
int file_io_paths(const char *source, const char *destination);
// Сохранить существующий закрытый regular-файл без конечной ссылки; UTF-8, 0/-1.
int file_io_sync_file(const char *path);
// Сохранить каталог, содержащий path; без разделителя используется '.', для '/file' — '/'. UTF-8, 0/-1.
int file_io_sync_directory(const char *path);
// Переместить отдельный готовый файл с durable заменой destination; caller владеет source. 0/-1.
// Ошибка после rename не откатывает замену; сохраняется каталог назначения. Файлы/каталоги не подменяются извне.
int file_io_publish(const char *source, const char *destination);
// Создать эксклюзивный temp рядом с destination; структура заранее обнулена, даже после ошибки вызвать discard.
int file_io_temp_open(struct file_io_temp *output, const char *destination);
// Проверить flush/close, обнулить file при любом результате; temp остаётся собственностью output.
// flags=0 — flush/close и atomic rename; FILE_IO_DURABLE — также sync файла и каталога назначения.
int file_io_temp_open(struct file_io_temp *output, const char *destination, unsigned flags);
// Проверить flush/[sync]/close, обнулить file при любом результате; temp остаётся собственностью output.
int file_io_temp_finish(struct file_io_temp *output);
// Закрыть/удалить собственный temp; повторный вызов допустим. Ошибки очистки логируются.
void file_io_temp_discard(struct file_io_temp *output);
// Повторно проверить paths и заменить destination закрытым temp; одинаковые файлы отклоняются.
// Повторно проверить paths и заменить destination закрытым соседним temp; одинаковые файлы отклоняются. 0/-1.
// При ошибке до rename temporary сохраняется; после rename он пуст, в том числе при отказе directory sync.
int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination);
/* Удалить уже признанный сиротой файл, атомарно проверив владение file_io_temp во всех потоках.
* 1 — удалён, 0 — активный temp или уже отсутствует, -1 — ошибка (логируется).
+65 -17
View File
@@ -23,11 +23,13 @@ enum { AK_REQUEST = 1, AK_REQUEST_ACK, AK_GRANT, AK_DENY, AK_RESULT_ACK };
struct ak_file {
struct UTUN_INSTANCE* inst;
char channel[32], source[1024], cipher[1024], plain[1024], temporary[1040], type[32];
char channel[32], source[1024], cipher[1024], plain[1024], type[32];
struct file_io_temp output; // Единственный собственный temp; стабильный адрес до completion.
uint8_t id[16], key[32], message[32], hash[32], reply[32];
struct attachment_info info;
uint64_t author, request;
int encrypt, error;
int plain_prepared, published; // Только реально подготовленный snapshot и выполненный rename дают право очистки.
};
/* Все ошибки SQLite содержат конкретную операцию. */
@@ -94,10 +96,8 @@ invalid:
/* Разделённые деревья исключают раздачу plaintext и удаление его обходом orphan media. */
static int ak_paths(struct ak_file* job) {
const char* slash = strrchr(CC(job->inst)->db_path, '/');
char base[512], hex[33]; ak_hex(job->id, hex);
if (slash) snprintf(base, sizeof(base), "%.*s", (int)(slash - CC(job->inst)->db_path), CC(job->inst)->db_path);
else snprintf(base, sizeof(base), ".");
if (media_index_get_base(CC(job->inst)->db, base)) return -1;
int a = snprintf(job->cipher, sizeof(job->cipher), "%s/media/%s/%s.uenc", base, job->channel, hex);
int b = snprintf(job->plain, sizeof(job->plain), "%s/decrypted/%s/%s.file", base, job->channel, hex);
if (a < 0 || b < 0 || (size_t)a >= sizeof(job->cipher) || (size_t)b >= sizeof(job->plain)) {
@@ -121,12 +121,18 @@ static int ak_parent(const char* path) {
static void ak_file_work(void* arg) {
struct ak_file* job = arg; job->error = -1;
if (ak_parent(job->plain) || ak_parent(job->cipher)) return;
snprintf(job->temporary, sizeof(job->temporary), "%s.part", job->encrypt ? job->cipher : job->plain);
if (job->encrypt) {
if (ma_copy_file(job->source, job->plain) || encrypted_file_sync(job->plain) || encrypted_file_sync_directory(job->plain) ||
encrypted_file_write(job->plain, job->temporary, job->key, job->author, job->id, &job->info, job->type)) return;
} else if (encrypted_file_read(job->cipher, job->temporary, job->key, job->author, job->id, job->hash, &job->info, job->type)) return;
if (encrypted_file_publish(job->temporary, job->encrypt ? job->cipher : job->plain)) return;
if (ma_copy_file(job->source, job->plain)) return;
job->plain_prepared = 1;
if (file_io_sync_file(job->plain) || file_io_sync_directory(job->plain) ||
encrypted_file_write(job->plain, job->cipher, &job->output, job->key, job->author, job->id, &job->info, job->type)) return;
} else if (encrypted_file_read(job->cipher, job->plain, &job->output, job->key, job->author, job->id,
job->hash, &job->info, job->type)) return;
int error = file_io_temp_publish(&job->output, job->encrypt ? job->plain : job->cipher, job->encrypt ? job->cipher : job->plain);
job->published = !job->output.temporary[0]; // Rename уже выполнен и при последующем отказе directory sync.
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "attachment keys: output publication encrypt=%d installed=%d error=%d",
job->encrypt, job->published, error);
if (error) return;
job->error = 0;
}
@@ -174,11 +180,12 @@ static void ak_file_done(void* arg, int error) {
if (job->encrypt) chat_event_submitted(inst, job->request, 0);
}
if (job->encrypt && remove(job->source) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: prepared source cleanup failed");
if ((!success || !valid) && job->temporary[0] && remove(job->temporary) && errno != ENOENT)
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary cleanup failed");
file_io_temp_discard(&job->output);
if (!valid) {
if (remove(job->plain) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel plaintext cleanup failed");
if (job->encrypt && remove(job->cipher) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel ciphertext cleanup failed");
if (((job->encrypt && job->plain_prepared) || (!job->encrypt && job->published)) && remove(job->plain) && errno != ENOENT)
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel plaintext cleanup failed");
if (job->encrypt && job->published && remove(job->cipher) && errno != ENOENT)
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel ciphertext cleanup failed");
}
if (valid) {
if (success && !job->encrypt) chat_media_cache_enforce(inst);
@@ -544,9 +551,52 @@ static void ak_tick(void* arg) {
if (!CC(inst)->attachment_timer) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: timer allocation failed");
}
struct ak_temp_cleanup { const char* root; const char* extension; unsigned removed; };
/* Только <channel>/<UUID>.<uenc|file>.tmp.<UUID>; чужие имена и активные owners сохраняются. */
static void ak_remove_temp(void* arg, const char* path, int64_t size) {
(void)size;
struct ak_temp_cleanup* cleanup = arg;
size_t prefix = strlen(cleanup->root);
if (strncmp(path, cleanup->root, prefix) || path[prefix] != '/') return;
const char* relative = path + prefix + 1;
const char* slash = strchr(relative, '/');
if (!slash || slash - relative >= 20) return;
char channel[20]; memcpy(channel, relative, (size_t)(slash - relative)); channel[slash - relative] = 0;
if (!ak_channel(channel)) return;
const char* name = slash + 1;
const char* hex = "0123456789abcdef";
if (strlen(name) != 74 || strspn(name, hex) != 32 || memcmp(name + 32, cleanup->extension, 5) ||
memcmp(name + 37, ".tmp.", 5) || strspn(name + 42, hex) != 32) return;
int removed = file_io_remove_orphan(path);
if (removed > 0) cleanup->removed++;
else if (removed < 0) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary recovery failed path=%s", path);
}
/* И при crash до публикации, и при прерванной расшифровке temp не является готовым вложением. */
static void ak_recover_temps(struct UTUN_INSTANCE* inst) {
char base[MEDIA_BASE_MAX];
if (media_index_get_base(CC(inst)->db, base)) return;
const char* trees[] = {"media", "decrypted"};
for (unsigned i = 0; i < 2; i++) {
char root[MEDIA_BASE_MAX + 16]; snprintf(root, sizeof(root), "%s/%s", base, trees[i]);
struct file_io_info info;
int status = file_io_inspect(root, 0, &info);
if (status == 1) continue;
if (status < 0) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: cannot inspect temporary tree path=%s", root); continue; }
struct ak_temp_cleanup cleanup = {.root = root, .extension = i ? ".file" : ".uenc"};
if (ma_dir_walk(root, ak_remove_temp, &cleanup))
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary recovery incomplete path=%s removed=%u", root, cleanup.removed);
else DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary recovery path=%s removed=%u", root, cleanup.removed);
}
}
/* После crash/отмены убрать только собственные подготовки, не имеющие сообщения.
* Вызывается до запуска workers либо после их полного завершения. */
static void ak_recover_preparations(struct UTUN_INSTANCE* inst) {
ak_recover_temps(inst);
char base[MEDIA_BASE_MAX];
if (media_index_get_base(CC(inst)->db, base)) return;
char** channels = NULL; int count = 0, columns = 0;
if (sqlite3_get_table(CC(inst)->db, "SELECT channel_id FROM channels", &channels, &count, &columns, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: recovery channel scan failed: %s", sqlite3_errmsg(CC(inst)->db)); return;
@@ -566,16 +616,14 @@ static void ak_recover_preparations(struct UTUN_INSTANCE* inst) {
for (int row = 0; row < rows; row++) {
for (int field = 0; field < fields; field++) {
const char* path = paths[(row + 1) * fields + field];
char temporary[1040]; snprintf(temporary, sizeof(temporary), "%s.part", path);
if ((remove(path) && errno != ENOENT) || (remove(temporary) && errno != ENOENT)) {
if (remove(path) && errno != ENOENT) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: unfinished file removal failed path=%s: %s", path, strerror(errno)); failed = 1;
}
}
}
sqlite3_free_table(paths);
if (rows && !failed) {
const char* slash = strrchr(CC(inst)->db_path, '/');
unsigned prefix = slash ? (unsigned)(slash - CC(inst)->db_path) : 1;
unsigned prefix = (unsigned)strlen(base);
char cleanup[768];
snprintf(cleanup, sizeof(cleanup), "DELETE FROM media_files WHERE chat_id='%s' AND location IN"
"(SELECT substr(a.cipher_path,%u) FROM attachment_keys a WHERE %s)", channel, prefix + 2, condition);
+1 -3
View File
@@ -1683,7 +1683,7 @@ static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) {
for (int i = 0; i < rels.count; i++) u_free(rels.items[i]);
u_free(rels.items); return;
}
/* Ещё не опубликованные контейнеры и их временные файлы принадлежат crypto worker. */
/* Конечные контейнеры принадлежат attachment_keys; активные temp защищены file_io-реестром. */
if (cc->attachment_initialized) {
sqlite3_stmt* active = NULL;
if (sqlite3_prepare_v2(cc->db, "SELECT cipher_path FROM attachment_keys", -1, &active, NULL) == SQLITE_OK) {
@@ -1693,8 +1693,6 @@ static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) {
size_t prefix = strlen(media_base);
if (path && !strncmp(path, media_base, prefix) && path[prefix] == '/') {
cache_rel_add(&rels, path + prefix + 1);
char temporary[1040]; snprintf(temporary, sizeof(temporary), "%s.part", path + prefix + 1);
cache_rel_add(&rels, temporary);
}
}
if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted cache: active scan failed rc=%d", rc);
+5 -5
View File
@@ -154,7 +154,7 @@ static void dm_cleanup_pump(struct dm_media_state* media);
static void dm_cleanup_work(void* arg) {
struct dm_cleanup_job* job = arg;
job->error = dm_file_remove(job->path);
if (!job->error) job->error = encrypted_file_sync_directory(job->path);
if (!job->error) job->error = file_io_sync_directory(job->path);
}
static void dm_cleanup_done(void* arg, int error) {
@@ -488,7 +488,7 @@ static void dm_file_work(void* arg) {
struct dm_file_job* job = arg;
job->error = -1;
if (job->work == DM_FILE_DELETE) {
if (!dm_file_remove(job->cipher) && !dm_file_remove(job->temp)) job->error = encrypted_file_sync_directory(job->cipher);
if (!dm_file_remove(job->cipher) && !dm_file_remove(job->temp)) job->error = file_io_sync_directory(job->cipher);
return;
}
if (job->work == DM_FILE_PREPARE) {
@@ -530,9 +530,9 @@ done:
if (out && fclose(out)) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_media: worker output close failed"); job->error = -1; }
if (!job->error) {
if (job->work == DM_FILE_PREPARE) {
job->error = encrypted_file_publish(job->temp, job->cipher);
if (!job->error) job->error = encrypted_file_sync_directory(job->plain);
} else job->error = encrypted_file_publish(job->work == DM_FILE_CUSTODY ? job->cipher : job->temp, job->output);
job->error = file_io_publish(job->temp, job->cipher);
if (!job->error) job->error = file_io_sync_directory(job->plain);
} else job->error = file_io_publish(job->work == DM_FILE_CUSTODY ? job->cipher : job->temp, job->output);
}
}
+49 -79
View File
@@ -10,118 +10,85 @@
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#ifdef _WIN32
#include <io.h>
#endif
/* Сделать создание/удаление имён durable на POSIX. */
int encrypted_file_sync_directory(const char* path) {
#ifndef _WIN32
char parent[1024];
snprintf(parent, sizeof(parent), "%s", path);
char* slash = strrchr(parent, '/');
if (!slash) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: path without parent: %s", path); return -1; }
*slash = '\0';
int fd = open(parent, O_RDONLY | O_DIRECTORY);
if (fd < 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: directory open failed: %s: %s", parent, strerror(errno)); return -1; }
int rc = fsync(fd);
if (rc) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: directory sync failed: %s: %s", parent, strerror(errno));
close(fd);
return rc ? -1 : 0;
#else
(void)path;
return 0;
#endif
}
/* Windows MoveFileEx обеспечивает замену и write-through существующего артефакта. */
int encrypted_file_publish(const char* temp, const char* path) {
#ifdef _WIN32
if (!MoveFileExA(temp, path, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: publish failed %s -> %s winerr=%lu", temp, path, GetLastError());
return -1;
}
#else
if (rename(temp, path) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: publish failed %s -> %s: %s", temp, path, strerror(errno));
return -1;
}
#endif
return encrypted_file_sync_directory(path);
}
#define EF_HEADER 66
#define EF_META (32 + ATTACHMENT_WIRE_MAX)
/* Гарантировать сохранение файла перед публикацией или подтверждением. */
int encrypted_file_sync(const char* path) {
FILE* f = fopen(path, "rb+");
if (!f) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: sync open failed path=%s: %s", path, strerror(errno)); return -1; }
#ifdef _WIN32
int rc = _commit(_fileno(f));
#else
int rc = fsync(fileno(f));
#endif
if (fclose(f)) rc = -1;
if (rc) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: sync failed path=%s: %s", path, strerror(errno));
return rc ? -1 : 0;
}
/* Записать контейнер; частичный destination удаляется при любой ошибке. */
int encrypted_file_write(const char* source, const char* destination, const uint8_t key[32],
/* Подготовить контейнер в эксклюзивном temp; конечный target не открывается для записи. */
int encrypted_file_write(const char* source, const char* target, struct file_io_temp* output, const uint8_t key[32],
uint64_t author, const uint8_t id[16], const struct attachment_info* info, const char* type) {
if (!output || output->file || output->temporary[0]) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: write requires inactive output owner"); return -1;
}
FILE* in = NULL; FILE* out = NULL;
int rc = -1;
uint8_t header[EF_HEADER] = {0}, meta[EF_META] = {0}, cipher[EF_META + 16], nonce[13], hash[32];
size_t length = 0;
uint64_t size = 0;
if (!source || !destination || !strcmp(source, destination) || !key || !author || !id ||
const char* reason = "invalid arguments";
if (!source || !target || !key || !author || !id ||
!type || strlen(type) >= 32 || attachment_validate(info)) goto done;
reason = "source/target alias or invalid file type";
if (file_io_paths(source, target)) goto done;
reason = "cannot open source";
in = file_io_open_read(source);
if (!in) goto done;
reason = "metadata encoding/encryption failed";
int n = attachment_encode(info, meta + 32, ATTACHMENT_WIRE_MAX);
if (n < 0) goto done;
memcpy(meta, type, strlen(type));
media_cipher_nonce(author, id, UINT64_MAX, nonce);
if (media_ccm_encrypt(key, nonce, meta, (size_t)n + 32, cipher, &length)) goto done;
in = fopen(source, "rb"); out = fopen(destination, "wb");
if (!in || !out) goto done;
reason = "cannot create private output";
if (file_io_temp_open(output, target, FILE_IO_DURABLE)) goto done;
out = output->file;
reason = "container write/encryption failed";
memcpy(header, "UTENC001", 8); memcpy(header + 8, id, 16);
header[32] = (uint8_t)(length >> 8); header[33] = (uint8_t)length;
if (fwrite(header, 1, sizeof(header), out) != sizeof(header) || fwrite(cipher, 1, length, out) != length ||
media_encrypt_stream(key, author, id, in, out, &size, hash)) goto done;
for (unsigned i = 0; i < 8; i++) header[24 + i] = (uint8_t)(size >> (56 - i * 8));
memcpy(header + 34, hash, 32);
if (fseeko(out, 0, SEEK_SET) || fwrite(header, 1, sizeof(header), out) != sizeof(header) || fflush(out)) goto done;
reason = "cannot rewrite container header";
if (fseeko(out, 0, SEEK_SET) || fwrite(header, 1, sizeof(header), out) != sizeof(header)) goto done;
reason = "source close or output durable finish failed";
rc = 0;
done:
OPENSSL_cleanse(meta, sizeof(meta));
if (in && fclose(in)) rc = -1;
if (out && fclose(out)) rc = -1;
if (!rc) rc = encrypted_file_sync(destination);
if (!rc) rc = file_io_temp_finish(output);
if (rc) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: preparation failed source=%s: %s", source ? source : "?", strerror(errno));
if (out && remove(destination) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "encrypted file: partial cleanup failed");
} else DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "encrypted file: prepared author=%llu bytes=%llu", (unsigned long long)author,
(unsigned long long)size);
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: preparation rejected source=%s target=%s reason=%s",
source ? source : "?", target ? target : "?", reason);
file_io_temp_discard(output);
} else DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "encrypted file: prepared author=%llu bytes=%llu temp=%s",
(unsigned long long)author, (unsigned long long)size, output->temporary);
return rc;
}
/* Проверить подписанный hash, метаданные и все порции до разрешения публикации. */
int encrypted_file_read(const char* source, const char* destination, const uint8_t key[32], uint64_t author,
int encrypted_file_read(const char* source, const char* target, struct file_io_temp* output, const uint8_t key[32], uint64_t author,
const uint8_t id[16], const uint8_t hash[32], struct attachment_info* info, char type[32]) {
if (!output || output->file || output->temporary[0]) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: read requires inactive output owner"); return -1;
}
FILE* in = NULL; FILE* out = NULL;
struct attachment_info result;
char result_type[32];
int rc = -1;
uint8_t actual[32], header[EF_HEADER], meta[EF_META], cipher[EF_META + 16], nonce[13];
size_t length = 0;
uint64_t size = 0;
const char* reason = "invalid arguments";
if (!source || !destination || !strcmp(source, destination) || !key || !author || !id || !hash || !info || !type) goto done;
if (!source || !target || !key || !author || !id || !hash || !info || !type) goto done;
reason = "source/target alias or invalid file type";
if (file_io_paths(source, target)) goto done;
reason = "cannot hash ciphertext";
if (ma_sha256_file(source, actual)) goto done;
reason = "ciphertext does not match signed manifest hash";
if (CRYPTO_memcmp(actual, hash, 32)) goto done;
reason = "cannot open ciphertext or truncated header";
in = fopen(source, "rb");
in = file_io_open_read(source);
if (!in || fread(header, 1, sizeof(header), in) != sizeof(header)) goto done;
reason = "unsupported container format or mismatched UUID";
if (memcmp(header, "UTENC001", 8) || memcmp(header + 8, id, 16)) goto done;
@@ -134,14 +101,14 @@ int encrypted_file_read(const char* source, const char* destination, const uint8
if (media_ccm_decrypt(key, nonce, cipher, encrypted_size, meta, &length)) goto done;
reason = "invalid attachment metadata";
if (length < 32 ||
!memchr(meta, 0, 32) || !meta[0] || attachment_decode(meta + 32, length - 32, info)) goto done;
!memchr(meta, 0, 32) || !meta[0] || attachment_decode(meta + 32, length - 32, &result)) goto done;
reason = "invalid content type";
for (unsigned i = 0; i < 32 && meta[i]; i++) if (meta[i] < 33 || meta[i] > 126 || meta[i] == '"' || meta[i] == '\\') goto done;
memcpy(type, meta, 32);
memcpy(result_type, meta, 32);
for (unsigned i = 0; i < 8; i++) size = (size << 8) | header[24 + i];
out = fopen(destination, "wb");
reason = "cannot open temporary plaintext";
if (!out) goto done;
if (file_io_temp_open(output, target, FILE_IO_DURABLE)) goto done;
out = output->file;
reason = "file authentication, size or body hash failed";
if (media_decrypt_stream(key, author, id, size, header + 34, in, out)) goto done;
reason = "close or durable file sync failed";
@@ -149,12 +116,15 @@ int encrypted_file_read(const char* source, const char* destination, const uint8
done:
OPENSSL_cleanse(meta, sizeof(meta));
if (in && fclose(in)) rc = -1;
if (out && fclose(out)) rc = -1;
if (!rc) rc = encrypted_file_sync(destination);
if (!rc) rc = file_io_temp_finish(output);
if (rc) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: rejected source=%s reason=%s", source ? source : "?", reason);
if (out && remove(destination) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "encrypted file: rejected output cleanup failed");
} else DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "encrypted file: verified author=%llu bytes=%llu", (unsigned long long)author,
(unsigned long long)size);
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: rejected source=%s target=%s reason=%s",
source ? source : "?", target ? target : "?", reason);
file_io_temp_discard(output);
} else {
*info = result; memcpy(type, result_type, sizeof(result_type));
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "encrypted file: verified author=%llu bytes=%llu temp=%s",
(unsigned long long)author, (unsigned long long)size, output->temporary);
}
return rc;
}
+13 -9
View File
@@ -1,23 +1,27 @@
/* Контейнер группового вложения: публичный заголовок, зашифрованные метаданные и
* порции AES-256-CCM. Полный SHA256 контейнера берётся из подписанного сообщения.
* Все функции файловые, вызываются только в media_async worker. Выход decrypt —
* временный файл; публиковать его можно только после успеха и durable сохранения. */
* Вызывать только в media_async worker. Write/read готовят отдельный durable temp,
* конечное имя публикует владелец через file_io_temp_publish. Source не изменяется. */
#ifndef ENCRYPTED_FILE_H
#define ENCRYPTED_FILE_H
#include "attachment.h"
#include "../../lib/file_io.h"
#ifdef __cplusplus
extern "C" {
#endif
#define ENCRYPTED_ATTACHMENT_TYPE "application/x-utun-encrypted"
int encrypted_file_write(const char* source, const char* destination, const uint8_t key[32],
/* source/target — UTF-8 пути разных файлов; target отсутствует либо regular без конечной ссылки.
* output заранее обнулён и имеет стабильный адрес до publish/discard; активный output отклоняется без очистки.
* 0 — output содержит закрытый проверенный temp рядом с target, target ещё не изменён.
* -1 — ошибка; source/target сохранены, собственный temp передан discard (ошибки удаления логируются).
* Ключ — 32 байта, UUID — 16, author!=0.
* Пути и файлы не подменяются во время вызова; публикация повторно проверяет их идентичность. */
int encrypted_file_write(const char* source, const char* target, struct file_io_temp* output, const uint8_t key[32],
uint64_t author, const uint8_t id[16], const struct attachment_info* info, const char* type);
int encrypted_file_read(const char* source, const char* destination, const uint8_t key[32], uint64_t author,
/* Проверить подписанный hash всего контейнера, UUID, метаданные и все tags. Контракт output — как у write.
* info/type[32] присваиваются только при успехе; при ошибке прежние значения сохраняются. */
int encrypted_file_read(const char* source, const char* target, struct file_io_temp* output, const uint8_t key[32], uint64_t author,
const uint8_t id[16], const uint8_t hash[32], struct attachment_info* info, char type[32]);
/* Flush/fsync и close проверяет владелец; этот примитив сохраняет уже закрытый файл. */
int encrypted_file_sync(const char* path);
/* Durable rename (write-through на Windows) и сохранение каталога на POSIX. */
int encrypted_file_publish(const char* temporary, const char* path);
int encrypted_file_sync_directory(const char* path);
#ifdef __cplusplus
}
#endif
+1 -1
View File
@@ -164,7 +164,7 @@ int ma_copy_file(const char* source, const char* destination) {
int status = file_io_paths(source, destination);
uint64_t total = 0;
if (status == 0) phase = "create temporary";
if (status == 0 && file_io_temp_open(&output, destination)) status = -1;
if (status == 0 && file_io_temp_open(&output, destination, 0)) status = -1;
if (status == 0) {
uint8_t buffer[65536]; size_t count;
while (1) {
+1 -1
View File
@@ -277,7 +277,7 @@ struct media_block_source* md_block_create(struct media_delivery_ctx* md, uint64
source->entry = entry; source->md = md; source->refs = 1; source->owned = 1;
source->group_id = group_id; source->chunk = chunk; source->size = size;
memcpy(source->media_id, media_id, 16); memcpy(source->block_id, block_id, 16);
if (file_io_temp_open(&source->storage, destination) || file_io_temp_finish(&source->storage)) {
if (file_io_temp_open(&source->storage, destination, 0) || file_io_temp_finish(&source->storage)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "%s: block temp creation failed chunk=%u", MD_ID, chunk);
md_block_release(source); return NULL;
}
+1 -1
View File
@@ -633,7 +633,7 @@ static int md_dl_register_servable(struct media_download* dl) {
/* Запись назначения начинается только после проверки блоков. До publish старое имя сохраняет прежние байты. */
static int md_dl_assemble(struct media_download* dl) {
struct file_io_temp output = {0};
int failed = file_io_temp_open(&output, dl->dest_path);
int failed = file_io_temp_open(&output, dl->dest_path, 0);
uint64_t total = 0;
uint8_t buffer[65536];
for (int bi = 0; bi < dl->num_blocks && !failed; bi++) {
+67 -19
View File
@@ -29,7 +29,6 @@
// Constants from spec (adjusted for completeness)
#define MAX_INFLIGHT_BYTES 65536 // Initial window
#define ACK_DELAY_TB 20 // ACK timer delay (2ms in 0.1ms units)
#define BURST_DELAY_FACTOR 4 // Delay before burst
#define BURST_SIZE 5 // Packets in burst (1 delayed + 4 burst)
#define RTT_HISTORY_SIZE 10 // For jitter calc
@@ -56,7 +55,7 @@ static void etcp_schedule_send(struct ETCP_CONN* conn, int delay_tb) {
if (!conn || conn->close_requested || conn->state == 2 || conn->tx_token) return;
if (!conn->initialized || conn->session_required || conn->reinit_pending) return;
if (conn->tx_running && !delay_tb) return;
int work = conn->input_send_q->count || conn->input_queue->count || conn->ack_till_pending || conn->ack_request_pending;
int work = conn->input_send_q->count || conn->input_queue->count || conn->ack_till_deadline_us || conn->ack_request_pending;
for (struct ETCP_LINK* link = conn->links; !work && link; link = link->next)
work = link->burst_active || link->ack_reply_pending || (link->ack_q && link->ack_q->count);
if (!work) return;
@@ -463,6 +462,7 @@ void etcp_connection_close(struct ETCP_CONN* etcp) {
// Cancel active timers
if (etcp->ack_resp_timer) { uasync_cancel_timeout(etcp->instance->ua, etcp->ack_resp_timer); etcp->ack_resp_timer = NULL; }
etcp->ack_resp_deadline_us = etcp->ack_till_deadline_us = 0;
if (etcp->ack_window_timer) { uasync_cancel_timeout(etcp->instance->ua, etcp->ack_window_timer); etcp->ack_window_timer = NULL; }
routing_del_conn(etcp);
@@ -522,11 +522,12 @@ void etcp_conn_reset(struct ETCP_CONN* conn) {
for (struct ETCP_LINK* link = conn->links; link; link = link->next) etcp_tx_link_reset(link, 0);
etcp_cancel_send(conn);
if (conn->ack_resp_timer) { uasync_cancel_timeout(conn->instance->ua, conn->ack_resp_timer); conn->ack_resp_timer = NULL; }
conn->ack_resp_deadline_us = 0;
if (conn->ack_window_timer) { uasync_cancel_timeout(conn->instance->ua, conn->ack_window_timer); conn->ack_window_timer = NULL; }
queue_clear(conn->input_queue); queue_clear(conn->output_queue);
queue_clear(conn->input_send_q); queue_clear(conn->input_wait_ack); queue_clear(conn->recv_q);
if (conn->normalizer) pn_reset(conn->normalizer);
conn->ack_till_pending = conn->ack_request_pending = 0;
conn->ack_till_deadline_us = conn->ack_request_pending = 0;
conn->next_tx_id = 1; conn->last_rx_id = conn->last_delivered_id = conn->rx_ack_till = 0;
conn->got_initial_pkt = 0;
conn->unacked_bytes = conn->rtt_last = conn->rtt_avg_10 = conn->jitter = conn->last_rtt_cb_time = 0;
@@ -794,7 +795,7 @@ static void input_queue_try_resume(struct ETCP_CONN* etcp) {// при ACK
if (etcp->unacked_bytes < etcp->optimal_inflight) {
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "[%s] resume input queue: unacked_bytes=%u input_len=%zu", etcp->log_name,
etcp->unacked_bytes, etcp->input_queue->total_bytes);
queue_resume_callback(etcp->input_queue);// и только когда больше нечего отправлять - забираем новый пакет
queue_resume_callback(etcp->input_queue); // освобождённый общий бюджет допускает новые DATA и при непустой send_q
}
}
@@ -962,16 +963,48 @@ void etcp_on_link_down(struct ETCP_CONN* etcp, struct ETCP_LINK* down_link) {
static void ack_response_timer_cb(void* arg) {
struct ETCP_CONN* conn = arg;
conn->ack_resp_timer = NULL;
etcp_conn_wake_sender(conn);
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "[%s] ACK timer fired due_us=%llu now_us=%llu", conn->log_name,
(unsigned long long)conn->ack_resp_deadline_us, (unsigned long long)get_time_us());
conn->ack_resp_timer = NULL; conn->ack_resp_deadline_us = 0;
etcp_conn_schedule_ack(conn);
}
static void ack_schedule(struct ETCP_CONN* conn) {
if (conn->ack_resp_timer || conn->close_requested) return;
conn->ack_resp_timer = uasync_set_timeout(conn->instance->ua, ACK_DELAY_TB, conn, ack_response_timer_cb, "etcp_ack_resp");
/* Один таймер обслуживает ближайший абсолютный срок: новый RX не отодвигает старые ACK. */
void etcp_conn_schedule_ack(struct ETCP_CONN* conn) {
if (!conn) return;
uint64_t now = get_time_us(), deadline = 0;
if (!conn->close_requested && conn->state != 2 && conn->initialized && !conn->session_required && !conn->reinit_pending) {
deadline = conn->ack_till_deadline_us;
for (struct ETCP_LINK* link = conn->links; link; link = link->next) {
if (!link->initialized || !link->link_status) continue;
if (link->ack_reply_pending) { deadline = now; break; }
const struct ACK_PACKET* first = link->ack_q ? (struct ACK_PACKET*)link->ack_q->head : NULL;
if (first && (!deadline || first->received_at_us + ETCP_ACK_DELAY_US < deadline))
deadline = first->received_at_us + ETCP_ACK_DELAY_US;
}
}
if (conn->ack_resp_timer && conn->ack_resp_deadline_us == deadline && deadline > now) return;
if (conn->ack_resp_timer) {
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "[%s] ACK timer replace old_us=%llu next_us=%llu", conn->log_name,
(unsigned long long)conn->ack_resp_deadline_us, (unsigned long long)deadline);
uasync_cancel_timeout(conn->instance->ua, conn->ack_resp_timer); conn->ack_resp_timer = NULL;
}
conn->ack_resp_deadline_us = 0;
if (!deadline) return;
if (deadline <= now) { etcp_conn_wake_sender(conn); return; }
int delay = (int)((deadline - now + 99) / 100);
conn->ack_resp_timer = uasync_set_timeout(conn->instance->ua, delay, conn, ack_response_timer_cb, "etcp_ack_resp");
if (!conn->ack_resp_timer) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[%s] ACK delay timer allocation failed, requesting immediate send", conn->log_name);
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[%s] ACK timer OOM due_us=%llu delay_tb=%d; forcing urgent ACK", conn->log_name,
(unsigned long long)deadline, delay);
for (struct ETCP_LINK* link = conn->links; link; link = link->next)
if (link->initialized && link->link_status && link->ack_q && link->ack_q->count) link->ack_reply_pending = 1;
if (conn->ack_till_deadline_us) conn->ack_till_deadline_us = now;
etcp_conn_wake_sender(conn);
} else {
conn->ack_resp_deadline_us = deadline;
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "[%s] ACK timer arm due_us=%llu now_us=%llu delay_tb=%d", conn->log_name,
(unsigned long long)deadline, (unsigned long long)now, delay);
}
}
@@ -1099,6 +1132,8 @@ struct ETCP_DGRAM* etcp_request_pkt(struct ETCP_CONN* conn) {
if (conn->tx_state) conn->tx_state = ETCP_TX_STATE_LINK_WAIT;
return NULL;
}
etcp_conn_schedule_ack(conn);
if (conn->close_requested) return NULL;
struct ETCP_LINK* link = NULL;
struct INFLIGHT_PACKET* packet = send_queue_select(conn, &link);
if (!packet) {
@@ -1109,14 +1144,26 @@ struct ETCP_DGRAM* etcp_request_pkt(struct ETCP_CONN* conn) {
conn->session_required || conn->reinit_pending) return NULL;
if (pulled) packet = send_queue_select(conn, &link);
}
if (!link) {
for (struct ETCP_LINK* candidate = conn->links; candidate; candidate = candidate->next)
if (candidate->initialized && candidate->link_status &&
(candidate->ack_reply_pending || (candidate->ack_q && candidate->ack_q->count))) {
link = candidate; packet = NULL; break;
}
if (!link || !link->burst_active) {
uint64_t now = get_time_us();
struct ETCP_LINK* ack_link = NULL;
uint64_t first_due = UINT64_MAX;
for (struct ETCP_LINK* candidate = conn->links; candidate; candidate = candidate->next) {
if (!candidate->initialized || !candidate->link_status) continue;
const struct ACK_PACKET* first = candidate->ack_q ? (struct ACK_PACKET*)candidate->ack_q->head : NULL;
if (!candidate->ack_reply_pending && !first) continue;
uint64_t due = candidate->ack_reply_pending ? 0 : first->received_at_us + ETCP_ACK_DELAY_US;
if (due <= now && (!ack_link || due < first_due)) { ack_link = candidate; first_due = due; }
}
if (ack_link) {
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "[%s] ACK due path=%u deadline_us=%llu now_us=%llu piggyback=%u", conn->log_name,
ack_link->local_link_id, (unsigned long long)first_due, (unsigned long long)now, ack_link == link && packet != NULL);
if (ack_link != link) packet = NULL;
link = ack_link;
}
}
if (!link && (conn->ack_till_pending || conn->ack_request_pending)) {
if (!link && (conn->ack_request_pending ||
(conn->ack_till_deadline_us && conn->ack_till_deadline_us <= get_time_us()))) {
link = etcp_loadbalancer_select_link(conn, 0, NULL); packet = NULL;
}
if (!link) {
@@ -1272,7 +1319,8 @@ void etcp_output_try_assembly(struct ETCP_CONN* etcp) {
}
if (delivered_count > 0 && !etcp->close_requested && !etcp->reinit_pending) {
etcp->ack_till_pending = 1; ack_schedule(etcp);
if (!etcp->ack_till_deadline_us) etcp->ack_till_deadline_us = get_time_us() + ETCP_ACK_DELAY_US;
etcp_conn_schedule_ack(etcp);
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] delivered %u fragments (%u bytes), till=%u recv_q=%d",
etcp->log_name, delivered_count, delivered_bytes, etcp->last_delivered_id, etcp->recv_q->count);
}
@@ -1475,7 +1523,7 @@ void etcp_conn_input(struct ETCP_DGRAM* pkt) {
pkt->link->rx_data_bytes += payload_len; pkt->link->rx_data_time_us = get_time_us();
}
etcp->got_initial_pkt = 1;
if (etcp_tx_queue_ack(pkt->link, seq, attempt_id) == 0) ack_schedule(etcp);
if (etcp_tx_queue_ack(pkt->link, seq, attempt_id) == 0) etcp_conn_schedule_ack(etcp);
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] accepted DATA seq=%u attempt=%llu path=%u bytes=%u duplicate=%d",
etcp->log_name, seq, (unsigned long long)attempt_no, pkt->link->local_link_id, payload_len, duplicate);
if (distance == 1) etcp_output_try_assembly(etcp);
+12 -3
View File
@@ -66,6 +66,7 @@ uint16_t get_current_timestamp(void);
#define INFLIGHT_STATE_WAIT_SEND 1
#define INFLIGHT_INITIAL_HASH_SIZE 1024
#define ETCP_LINK_HISTORY_BYTES 32 // по одному биту для локального слота линка 0..255
#define ETCP_ACK_DELAY_US 2000 // обычный ACK-only: срок от старейшей записи; piggyback и срочный ответ не ждут
#define MAX_INFLIGHT_SIZE 16384 // максимальное число элементов в inflight приёмной очереди (для предотвращения атак)
#define ETCP_ACK_GAP_MAX_PACKETS 8192 // предел кумулятивного ACK за приём: 8192 × PACKET_DATA_SIZE(2048) = 16 MB (защита от DoS)
@@ -197,9 +198,10 @@ struct ETCP_CONN {
uint32_t optimal_inflight; // сумма индивидуальных окон; общий бюджет подгрузки новых DATA с учётом unacked_bytes
// Timers
void* ack_resp_timer; // ACK send timer
void* ack_resp_timer; // один таймер ближайшего срока ACK всех линков/логической границы
uint64_t ack_resp_deadline_us; // абсолютный срок установленного таймера; 0 без таймера
void* ack_window_timer; // запрос потерянного till, только при заполненном приёмном окне
uint8_t ack_till_pending; // нужно передать текущую логическую границу по рабочему пути
uint64_t ack_till_deadline_us; // срок передачи текущей логической границы по рабочему пути; 0 — нет запроса
uint8_t ack_request_pending; // нужно запросить границу у пира
void* down_timer; // удаление после непрерывного DOWN, включая первоначальное ожидание
uint64_t down_deadline_tb; // сохраняется при наличии NCD handles после истечения таймера
@@ -342,7 +344,9 @@ void etcp_conn_input(struct ETCP_DGRAM* pkt);
int etcp_int_send(struct ETCP_CONN* etcp, const void* data, uint16_t len);
/* Подготовить следующий пакет из pkt_pool либо NULL при ожидании/ошибке/отсутствии работы.
* Сканирует send_q: готовые повторы имеют приоритет, заблокированные записи пропускаются.
* Наступивший/срочный ACK своего пути имеет приоритет; готовый DATA этого пути забирает его piggyback.
* Сканирует send_q: готовые повторы идут перед новыми DATA, заблокированные записи пропускаются.
* Короткая измерительная burst-серия завершается до перехода к ACK другого пути.
* При отсутствии кандидата допускает одну подгрузку в рамках общего/приёмного окна; callback может сделать reset/close.
* Пакет с DATA содержит резерв попытки: etcp_loadbalancer_send завершает send/rollback и возвращает пакет в пул. */
struct ETCP_DGRAM* etcp_request_pkt(struct ETCP_CONN* etcp);
@@ -351,6 +355,11 @@ struct ETCP_DGRAM* etcp_request_pkt(struct ETCP_CONN* etcp);
* Используется очередями, ACK/RTO, изменением окна и подтверждением сессии. close/reset отменяет запуск. */
void etcp_conn_wake_sender(struct ETCP_CONN* conn);
/* Пересчитать единственный ACK-таймер по головам очередей и ack_till_deadline_us; просроченную работу передать sender.
* Вызвать после RX/продвижения till, завершения send и смены доступных путей. NULL/закрытый conn допустимы.
* Piggyback не ждёт сроков. При OOM таймера существующие флаги срочного ACK обеспечивают немедленный fallback. */
void etcp_conn_schedule_ack(struct ETCP_CONN* conn);
// Process ACK receipt - remove acknowledged packet from inflight queues
void etcp_ack_recv(struct ETCP_CONN* etcp, uint32_t seq);
+1 -1
View File
@@ -264,7 +264,7 @@ struct ETCP_LINK {
void* shaper_timer;
struct ll_queue* attempts; // Физические попытки, внутренний ключ (seq<<16)|attempt_no; ll.len учитывается в окне
struct ll_queue* ack_q; // ACK только DATA этого линка, тот же составной ключ
uint8_t ack_reply_pending; // Ответ на запрос till возвращается тем же путём, даже при пустой ACK-очереди
uint8_t ack_reply_pending; // срочный ACK: ответ till тем же путём либо fallback при OOM таймера; обходит ACK_DELAY
void* retrans_timer; // Срок старейшей принятой транспортом попытки этого канала
uint64_t retrans_deadline_us; // Уже запланированная проверка: более ранний таймер можно сохранить
uint32_t data_rtt_us; // Сглаженный RTT DATA->ACK этого пути; 0 до первого измерения
+3 -2
View File
@@ -186,16 +186,17 @@ struct ETCP_TX_ATTEMPT* etcp_tx_prepare(struct ETCP_LINK* link, struct INFLIGHT_
}
void etcp_tx_complete(struct ETCP_DGRAM* dgram, int success) {
struct ETCP_CONN* conn = dgram->link->etcp;
if (success) {
dgram->link->ack_reply_pending = 0;
dgram->link->etcp->ack_till_pending = 0;
conn->ack_till_deadline_us = 0;
if (dgram->ack_request) dgram->link->etcp->ack_request_pending = 0;
}
etcp_conn_schedule_ack(conn);
struct ETCP_TX_ATTEMPT* attempt = dgram->attempt;
if (!attempt) return;
dgram->attempt = NULL;
struct ETCP_LINK* link = dgram->link;
struct ETCP_CONN* conn = link->etcp;
struct INFLIGHT_PACKET* packet = logical_find(conn, attempt->seq);
if (!success) { attempt_retire(link, attempt, 0, 0); return; }
if (!packet || packet->state != INFLIGHT_STATE_WAIT_SEND) {
+3 -1
View File
@@ -54,6 +54,7 @@ uint32_t etcp_tx_rto_us(const struct ETCP_LINK* link);
struct ETCP_TX_ATTEMPT* etcp_tx_prepare(struct ETCP_LINK* link, struct INFLIGHT_PACKET* packet);
/* Завершить синхронный send: success=1 принят транспортом, 0 — снять резерв без сетевой потери.
* Принятие DATA фиксирует историю каналов и RR; повторно использованный слот начинает новый обход.
* После потребления ACK пересчитывает ближайший срок оставшихся очередей; успешный пакет снимает pending till/urgent этого пути.
* Вызвать ровно один раз до освобождения dgram и до следующего poll; dgram->attempt обнуляется. */
void etcp_tx_complete(struct ETCP_DGRAM* dgram, int success);
/* Принятый ACK: seq подтверждает логическую доставку; внутренний attempt_id=(seq<<16)|номер обновляет только свой линк.
@@ -66,7 +67,8 @@ void etcp_tx_complete(struct ETCP_DGRAM* dgram, int success);
* Непредставимый интервал не создаёт оценку BW. Пустой flight заново начинает приёмную базу, исключая простой. */
void etcp_tx_ack(struct ETCP_LINK* link, uint32_t seq, uint64_t attempt_id, uint32_t receiver_delay_us,
uint64_t peer_rx_bytes, uint64_t peer_rx_time_us);
/* Поставить ACK уже сохранённого/доставленного DATA в очередь этого линка; 0/-1, ошибка логируется. */
/* Поставить ACK уже сохранённого/доставленного DATA в очередь этого линка; 0/-1, ошибка логируется.
* received_at_us сохраняется для RTT и срока ACK-only; вызывающий затем запускает etcp_conn_schedule_ack(). */
int etcp_tx_queue_ack(struct ETCP_LINK* link, uint32_t seq, uint64_t attempt_id);
#endif
+2 -2
View File
@@ -713,7 +713,7 @@ static int vc_run(struct vc_ctx* ctx) {
static void vc_thread_entry(void* raw) {
struct vc_ctx* ctx = raw;
video_ffmpeg_init();
if (file_io_paths(ctx->source, ctx->destination) || file_io_temp_open(&ctx->file, ctx->destination)) { ctx->error = -1; return; }
if (file_io_paths(ctx->source, ctx->destination) || file_io_temp_open(&ctx->file, ctx->destination, 0)) { ctx->error = -1; return; }
struct stat info;
if (!stat(ctx->source, &info)) ctx->source_size = info.st_size;
int error = vc_run(ctx);
@@ -844,7 +844,7 @@ int video_extract_thumbnail(const char* src, const char* dst, int max_w) {
}
if (error < 0) goto done;
phase = "write JPEG";
if (file_io_temp_open(&output, dst)) { error = AVERROR(EIO); goto done; }
if (file_io_temp_open(&output, dst, 0)) { error = AVERROR(EIO); goto done; }
if (fwrite(packet->data, 1, packet->size, output.file) != (size_t)packet->size) {
error = AVERROR(errno ? errno : EIO); goto done;
}
+11 -2
View File
@@ -390,6 +390,11 @@ test_attachment_SOURCES = test_attachment.c
test_attachment_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_encrypted_file_SOURCES = test_encrypted_file.c
test_encrypted_file_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
if OS_LINUX
test_encrypted_file_LDFLAGS = -Wl,--wrap=fopen -Wl,--wrap=fdopen -Wl,--wrap=fread -Wl,--wrap=__fread_chk \
-Wl,--wrap=fwrite -Wl,--wrap=ferror -Wl,--wrap=fflush -Wl,--wrap=fclose -Wl,--wrap=fseeko \
-Wl,--wrap=fsync -Wl,--wrap=rename -Wl,--wrap=random_bytes
endif
test_encrypted_attachment_SOURCES = test_encrypted_attachment.c $(test_clock_sources)
test_encrypted_attachment_CFLAGS = $(test_dm_e2e_CFLAGS)
test_encrypted_attachment_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
@@ -399,6 +404,9 @@ test_encrypted_attachment_LDFLAGS += -Wl,--wrap=uasync_set_timeout -Wl,--wrap=me
endif
test_attachment_send_SOURCES = test_attachment_send.c
test_attachment_send_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
if OS_LINUX
test_attachment_send_LDFLAGS = -Wl,--wrap=media_async_submit
endif
test_dm_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/lib
test_dm_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
@@ -900,7 +908,7 @@ if OS_LINUX
test_media_primitives_LDFLAGS = -Wl,--wrap=fopen -Wl,--wrap=fdopen -Wl,--wrap=fread -Wl,--wrap=__fread_chk -Wl,--wrap=ferror \
-Wl,--wrap=fwrite -Wl,--wrap=fflush -Wl,--wrap=fclose -Wl,--wrap=rename -Wl,--wrap=RAND_bytes \
-Wl,--wrap=opendir -Wl,--wrap=readdir -Wl,--wrap=closedir -Wl,--wrap=fseeko \
-Wl,--wrap=pthread_create -Wl,--wrap=u_calloc_impl -Wl,--wrap=uasync_post_reserved
-Wl,--wrap=pthread_create -Wl,--wrap=u_calloc_impl -Wl,--wrap=uasync_post_reserved -Wl,--wrap=fsync
endif
test_media_index_SOURCES = test_media_index.c
@@ -1065,7 +1073,8 @@ test_etcp_multipath_LDFLAGS = $(test_clock_ldflags) -Wl,--wrap=etcp_dump_pkt_sec
test_stcp_sigpipe_SOURCES = test_stcp_sigpipe.c
test_stcp_sigpipe_LDADD = $(LIBUTUN) $(COMMON_LIBS)
test_etcp_windows_LDADD = $(LIBUTUN) $(COMMON_LIBS)
test_etcp_windows_LDFLAGS = $(test_clock_ldflags) -Wl,--wrap=get_time_tb -Wl,--wrap=etcp_encrypt_send -Wl,--wrap=memory_pool_alloc_impl \
test_etcp_windows_LDFLAGS = $(test_clock_ldflags) -Wl,--wrap=get_time_tb -Wl,--wrap=get_time_us \
-Wl,--wrap=etcp_encrypt_send -Wl,--wrap=memory_pool_alloc_impl \
-Wl,--wrap=bbr_main -Wl,--wrap=uasync_set_timeout -Wl,--wrap=uasync_call_soon -Wl,--wrap=ll_alloc_lldgram \
-Wl,--wrap=u_malloc_impl -Wl,--wrap=u_calloc_impl -Wl,--wrap=u_realloc_impl -Wl,--wrap=random_bytes
+40
View File
@@ -11,6 +11,7 @@
#include "../src/dm/dm_core.h"
#include "../src/dm/dm_crypto.h"
#include "../src/media_async/attachment_send.h"
#include "../src/media_async/media_async.h"
#include "test_utils.h"
#define REQUIRE(value) do { if (!(value)) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment send check line=%d", __LINE__); return 1; } } while (0)
@@ -22,6 +23,27 @@ struct test_state {
};
static struct test_state test;
#ifdef __linux__
static pthread_barrier_t prepared_barrier;
static int capture_work;
static ma_work_fn captured_work;
void __real_media_async_submit(struct media_async*, struct UASYNC*, ma_work_fn, void*, ma_done_fn, void*);
static void prepared_work(void* arg) {
captured_work(arg);
pthread_barrier_wait(&prepared_barrier);
}
/* Завершить настоящий crypto worker, сохранив completion до остановки сервиса. */
void __wrap_media_async_submit(struct media_async* ma, struct UASYNC* ua, ma_work_fn work, void* data, ma_done_fn done, void* arg) {
if (capture_work) { capture_work = 0; captured_work = work; work = prepared_work; }
__real_media_async_submit(ma, ua, work, data, done, arg);
}
#endif
static void count_temps(void* arg, const char* path, int64_t size) {
(void)size;
if (strstr(path, ".tmp.")) (*(unsigned*)arg)++;
}
static void release_pcm(void* owner) {
struct test_state* state = owner;
state->released++; state->release_in_worker = !pthread_equal(pthread_self(), state->event_thread);
@@ -89,17 +111,35 @@ int main(void) {
char prepared[1024]; snprintf(prepared, sizeof(prepared), "%s/cancel-encrypted.bin", dir);
FILE* prepared_file = fopen(prepared, "wb"); REQUIRE(prepared_file && fputc(42, prepared_file) != EOF && !fclose(prepared_file));
struct attachment_info encrypted_info = { .kind = ATTACHMENT_FILE, .name = "cancel.bin" }; uint8_t no_reply[32] = {0};
#ifdef __linux__
REQUIRE(!pthread_barrier_init(&prepared_barrier, NULL, 2)); capture_work = 1;
#endif
REQUIRE(!chat_attachment_send(inst, channel_id, prepared, &encrypted_info, "application/octet-stream", no_reply, 0));
sqlite3_stmt* paths = NULL;
REQUIRE(!sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT cipher_path,plain_path FROM attachment_keys", -1, &paths, NULL));
REQUIRE(sqlite3_step(paths) == SQLITE_ROW);
char cancelled_cipher[1024], cancelled_plain[1024];
snprintf(cancelled_cipher, sizeof(cancelled_cipher), "%s", sqlite3_column_text(paths, 0));
snprintf(cancelled_plain, sizeof(cancelled_plain), "%s", sqlite3_column_text(paths, 1)); sqlite3_finalize(paths);
#ifdef __linux__
pthread_barrier_wait(&prepared_barrier);
REQUIRE(ma_file_size(cancelled_cipher) > 0 && ma_file_size(cancelled_plain) == 1);
#endif
req = u_calloc(1, sizeof(*req)); REQUIRE(req);
req->inst = inst; req->is_dm = 1; req->info.kind = ATTACHMENT_VOICE; req->preset = 1;
snprintf(req->target, sizeof(req->target), "%s", conv);
req->pcm = test.pcm; req->pcm_count = 48000; req->pcm_owner = &test; req->pcm_release = release_pcm;
attachment_send_trampoline(req);
chat_service_stop(inst);
#ifdef __linux__
REQUIRE(!pthread_barrier_destroy(&prepared_barrier));
#endif
REQUIRE(test.released == 2 && test.failed == 1 && count(inst) == 1);
sqlite3_stmt* cancelled_key = NULL;
REQUIRE(!sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT count(*) FROM attachment_keys", -1, &cancelled_key, NULL));
REQUIRE(sqlite3_step(cancelled_key) == SQLITE_ROW && sqlite3_column_int(cancelled_key, 0) == 0); sqlite3_finalize(cancelled_key);
REQUIRE(ma_file_size(cancelled_cipher) < 0 && ma_file_size(cancelled_plain) < 0 && ma_file_size(prepared) < 0);
unsigned temps = 0; REQUIRE(!ma_dir_walk(dir, count_temps, &temps) && !temps);
uasync_poll(ua, 0); REQUIRE(test.failed == 1);
/* Отклонение после stop удаляет только переданный собственный cache. */
+41 -1
View File
@@ -96,6 +96,46 @@ void __wrap_media_download_handle_chunk(struct UTUN_INSTANCE* inst, uint64_t gro
static void hex(const uint8_t* p, size_t len, char* out) {
for (size_t i = 0; i < len; i++) snprintf(out + i * 2, 3, "%02x", p[i]);
}
/* Startup/restart удаляет только строгие собственные stale temp; живые owners обоих деревьев сохраняются. */
static int start_with_recovery(struct UTUN_INSTANCE* inst, const char* base) {
struct file_io_temp live[2] = {0};
char target[2][1024], stale[2][1100], foreign[2][1100];
const char* trees[] = {"media", "decrypted"};
const char* extensions[] = {"uenc", "file"};
int result = -1;
for (unsigned i = 0; i < 2; i++) {
char directory[900]; snprintf(directory, sizeof(directory), "%s/%s", base, trees[i]);
if (utun_mkdir(directory, 0700) && errno != EEXIST) goto done;
snprintf(directory, sizeof(directory), "%s/%s/123", base, trees[i]);
if (utun_mkdir(directory, 0700) && errno != EEXIST) goto done;
snprintf(target[i], sizeof(target[i]), "%s/00000000000000000000000000000001.%s", directory, extensions[i]);
snprintf(stale[i], sizeof(stale[i]), "%s.tmp.00000000000000000000000000000002", target[i]);
snprintf(foreign[i], sizeof(foreign[i]), "%s.tmp.foreign", target[i]);
const char* paths[] = {target[i], stale[i], foreign[i]};
for (unsigned j = 0; j < 3; j++) {
FILE* file = fopen(paths[j], "wb");
if (!file) goto done;
int failed = fputs("retained", file) < 0;
if (fclose(file)) failed = 1;
if (failed) goto done;
}
if (file_io_temp_open(&live[i], target[i], FILE_IO_DURABLE) || fputs("live", live[i].file) < 0) goto done;
if (i ? file_io_temp_finish(&live[i]) : fflush(live[i].file)) goto done;
}
if (utun_instance_init(inst)) goto done;
for (unsigned i = 0; i < 2; i++) {
if (ma_file_size(stale[i]) >= 0 || ma_file_size(target[i]) != 8 || ma_file_size(foreign[i]) != 8 ||
ma_file_size(live[i].temporary) != 4 || file_io_remove_orphan(live[i].temporary) != 0) goto done;
}
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS attachment restart recovery: both trees, open/finished owners, final and foreign files");
result = 0;
done:
for (unsigned i = 0; i < 2; i++) file_io_temp_discard(&live[i]);
if (result) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment recovery fixture failed base=%s errno=%d", base, errno);
return result;
}
static int start_node(struct media_test* t, int index) {
char path[768], pub[65], priv[65], server[65], db[768];
hex(t->keys[index].public_key, 32, pub); hex(t->keys[index].private_key, 32, priv);
@@ -117,7 +157,7 @@ static int start_node(struct media_test* t, int index) {
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "encrypted attachment fixture: node=%d port=%u retained for restart", index, t->ports[index]);
}
if (t->nodes[index] && index != S) test_etcp_set_peer(t->nodes[index], "to_s", t->nodes[S], "s1");
return t->nodes[index] ? utun_instance_init(t->nodes[index]) : -1;
return t->nodes[index] ? start_with_recovery(t->nodes[index], db) : -1;
}
/* Фикстура создаёт канал напрямую, поэтому сама снимает gate после реального Merkle round. */
+183 -9
View File
@@ -6,6 +6,79 @@
#include "test_utils.h"
#include <assert.h>
#include <openssl/rand.h>
#include <errno.h>
#ifdef __linux__
#include <dirent.h>
enum fault { F_NONE, F_OPEN, F_FDOPEN, F_READ, F_WRITE, F_SEEK, F_FLUSH, F_INPUT_CLOSE, F_OUTPUT_CLOSE,
F_SYNC, F_RENAME, F_DIRECTORY, F_RANDOM };
static enum fault fault;
static int injected, fault_after, read_failed;
static FILE *input_file, *output_file;
FILE* __real_fopen(const char*, const char*);
FILE* __real_fdopen(int, const char*);
size_t __real_fread(void*, size_t, size_t, FILE*), __real_fwrite(const void*, size_t, size_t, FILE*);
#ifdef __GLIBC__
size_t __real___fread_chk(void*, size_t, size_t, size_t, FILE*);
#endif
int __real_ferror(FILE*), __real_fflush(FILE*), __real_fclose(FILE*), __real_fsync(int);
int __real_fseeko(FILE*, off_t, int), __real_rename(const char*, const char*);
int __real_random_bytes(uint8_t*, size_t);
static int inject(enum fault value) {
if (fault != value || injected || --fault_after > 0) return 0;
injected = 1; errno = EIO; return 1;
}
FILE* __wrap_fopen(const char* path, const char* mode) {
if (!strcmp(mode, "rb") && inject(F_OPEN)) return NULL;
FILE* file = __real_fopen(path, mode);
if (!strcmp(mode, "rb")) input_file = file;
return file;
}
FILE* __wrap_fdopen(int fd, const char* mode) {
if (inject(F_FDOPEN)) return NULL;
return output_file = __real_fdopen(fd, mode);
}
size_t __wrap_fread(void* data, size_t size, size_t count, FILE* file) {
if (file == input_file && inject(F_READ)) { read_failed = 1; return 0; }
return __real_fread(data, size, count, file);
}
#ifdef __GLIBC__
size_t __wrap___fread_chk(void* data, size_t cap, size_t size, size_t count, FILE* file) {
if (file == input_file && inject(F_READ)) { read_failed = 1; return 0; }
return __real___fread_chk(data, cap, size, count, file);
}
#endif
int __wrap_ferror(FILE* file) { return file == input_file && read_failed ? 1 : __real_ferror(file); }
size_t __wrap_fwrite(const void* data, size_t size, size_t count, FILE* file) {
return file == output_file && inject(F_WRITE) ? 0 : __real_fwrite(data, size, count, file);
}
int __wrap_fflush(FILE* file) { return file == output_file && inject(F_FLUSH) ? EOF : __real_fflush(file); }
int __wrap_fseeko(FILE* file, off_t offset, int whence) { return inject(F_SEEK) ? -1 : __real_fseeko(file, offset, whence); }
int __wrap_fclose(FILE* file) {
int error = (file == input_file && inject(F_INPUT_CLOSE)) || (file == output_file && inject(F_OUTPUT_CLOSE));
if (file == input_file) { input_file = NULL; read_failed = 0; }
if (file == output_file) output_file = NULL;
int result = __real_fclose(file);
if (error) { errno = EIO; return EOF; }
return result;
}
int __wrap_fsync(int fd) {
struct stat data; assert(!fstat(fd, &data));
return inject(S_ISDIR(data.st_mode) ? F_DIRECTORY : F_SYNC) ? -1 : __real_fsync(fd);
}
int __wrap_rename(const char* from, const char* to) { return inject(F_RENAME) ? -1 : __real_rename(from, to); }
int __wrap_random_bytes(uint8_t* data, size_t len) { return inject(F_RANDOM) ? -1 : __real_random_bytes(data, len); }
static unsigned count_fds(void) {
DIR* dir = opendir("/proc/self/fd"); assert(dir);
unsigned count = 0;
while (readdir(dir)) count++;
assert(!closedir(dir)); return count;
}
#endif
static void hash_same(const char* path, const uint8_t expected[32]) {
uint8_t actual[32]; assert(!ma_sha256_file(path, actual) && !memcmp(actual, expected, 32));
}
static void roundtrip(const char* dir, unsigned size) {
char source[768], cipher[768], output[768];
@@ -17,37 +90,138 @@ static void roundtrip(const char* dir, unsigned size) {
uint8_t key[32], wrong[32], id[16], hash[32];
assert(RAND_bytes(key, 32) == 1 && RAND_bytes(wrong, 32) == 1 && RAND_bytes(id, 16) == 1);
struct attachment_info info = { .kind = ATTACHMENT_FILE, .name = "секретный-документ.bin" }, result;
struct file_io_temp temporary = {0};
char type[32];
assert(!encrypted_file_write(source, cipher, key, 42, id, &info, "application/octet-stream"));
assert(!encrypted_file_write(source, cipher, &temporary, key, 42, id, &info, "application/octet-stream"));
assert(file_io_remove_orphan(temporary.temporary) == 0);
assert(!file_io_temp_publish(&temporary, source, cipher)); file_io_temp_discard(&temporary);
assert(!ma_sha256_file(cipher, hash));
assert(!encrypted_file_read(cipher, output, key, 42, id, hash, &result, type));
assert(!encrypted_file_read(cipher, output, &temporary, key, 42, id, hash, &result, type));
assert(ma_file_size(output) < 0 && file_io_remove_orphan(temporary.temporary) == 0);
assert(!file_io_temp_publish(&temporary, cipher, output)); file_io_temp_discard(&temporary);
assert(!strcmp(info.name, result.name) && !strcmp(type, "application/octet-stream"));
f = fopen(output, "rb"); assert(f);
for (unsigned i = 0; i < size; i++) assert(fgetc(f) == (int)((i * 73 + 19) & 255));
assert(fgetc(f) == EOF); assert(!fclose(f)); assert(!remove(output));
assert(encrypted_file_read(cipher, output, wrong, 42, id, hash, &result, type) < 0);
assert(encrypted_file_read(cipher, output, &temporary, wrong, 42, id, hash, &result, type) < 0);
assert(ma_file_size(output) < 0);
assert(encrypted_file_read(cipher, output, key, 43, id, hash, &result, type) < 0);
assert(encrypted_file_read(cipher, output, &temporary, key, 43, id, hash, &result, type) < 0);
id[0] ^= 1;
assert(encrypted_file_read(cipher, output, key, 42, id, hash, &result, type) < 0); id[0] ^= 1;
assert(encrypted_file_read(cipher, output, &temporary, key, 42, id, hash, &result, type) < 0); id[0] ^= 1;
/* Подмена metadata даже с заново вычисленным внешним hash не проходит CCM. */
f = fopen(cipher, "rb+"); assert(f && !fseek(f, 70, SEEK_SET));
int c = fgetc(f); assert(c != EOF && !fseek(f, 70, SEEK_SET) && fputc(c ^ 1, f) != EOF && !fclose(f));
assert(encrypted_file_read(cipher, output, key, 42, id, hash, &result, type) < 0);
assert(encrypted_file_read(cipher, output, &temporary, key, 42, id, hash, &result, type) < 0);
assert(!ma_sha256_file(cipher, hash));
assert(encrypted_file_read(cipher, output, key, 42, id, hash, &result, type) < 0 && ma_file_size(output) < 0);
assert(!encrypted_file_write(source, cipher, key, 42, id, &info, "application/octet-stream"));
assert(encrypted_file_read(cipher, output, &temporary, key, 42, id, hash, &result, type) < 0 && ma_file_size(output) < 0);
assert(!encrypted_file_write(source, cipher, &temporary, key, 42, id, &info, "application/octet-stream"));
assert(!file_io_temp_publish(&temporary, source, cipher)); file_io_temp_discard(&temporary);
/* Обрезанный контейнер и лишние байты отвергаются независимо от manifest hash. */
f = fopen(cipher, "ab"); assert(f && fputc(0, f) != EOF && !fclose(f));
assert(!ma_sha256_file(cipher, hash));
assert(encrypted_file_read(cipher, output, key, 42, id, hash, &result, type) < 0 && ma_file_size(output) < 0);
assert(encrypted_file_read(cipher, output, &temporary, key, 42, id, hash, &result, type) < 0 && ma_file_size(output) < 0);
printf("PASS: encrypted container size=%u, metadata, identity, key and integrity checks\n", size);
}
/* Оба API отвергают разные имена одного файла, не меняя содержимое и не удаляя alias. */
static void aliases(const char* dir) {
char source[768], cipher[768], target[768];
snprintf(source, sizeof(source), "%s/source", dir); snprintf(cipher, sizeof(cipher), "%s/cipher", dir);
uint8_t key[32] = {1}, id[16] = {2}, hash[32], before[32];
struct attachment_info info = {.kind = ATTACHMENT_FILE, .name = "example.bin"}, result;
struct file_io_temp temporary = {0};
char type[32];
assert(!encrypted_file_write(source, cipher, &temporary, key, 42, id, &info, "application/octet-stream"));
assert(!file_io_temp_publish(&temporary, source, cipher));
for (int decrypt = 0; decrypt < 2; decrypt++) {
const char* input = decrypt ? cipher : source;
assert(!ma_sha256_file(input, before));
for (int kind = 0; kind < 4; kind++) {
if (!kind) snprintf(target, sizeof(target), "%s", input);
else if (kind == 1) snprintf(target, sizeof(target), "%s/./%s", dir, decrypt ? "cipher" : "source");
else {
#ifndef _WIN32
snprintf(target, sizeof(target), "%s/alias", dir);
assert(kind == 2 ? !link(input, target) : !symlink(input, target));
#else
continue;
#endif
}
int error = decrypt ? encrypted_file_read(input, target, &temporary, key, 42, id, before, &result, type)
: encrypted_file_write(input, target, &temporary, key, 42, id, &info, "application/octet-stream");
assert(error < 0 && !temporary.file && !temporary.temporary[0]);
hash_same(input, before); hash_same(target, before);
if (kind >= 2) assert(!remove(target));
}
}
/* Активный output чужой операции не теряется даже при ошибочных аргументах. */
snprintf(target, sizeof(target), "%s/output", dir);
assert(!file_io_temp_open(&temporary, target, 0));
assert(encrypted_file_write(source, target, &temporary, key, 42, id, &info, "application/octet-stream") < 0);
assert(encrypted_file_read(cipher, target, &temporary, key, 42, id, before, &result, type) < 0);
assert(temporary.file && temporary.temporary[0] && file_io_remove_orphan(temporary.temporary) == 0);
file_io_temp_discard(&temporary);
/* Между подготовкой и publish цель стала alias исходника: второй контроль сохраняет его. */
#ifndef _WIN32
assert(!ma_sha256_file(source, before));
assert(!encrypted_file_write(source, target, &temporary, key, 42, id, &info, "application/octet-stream"));
assert(!link(source, target));
assert(file_io_temp_publish(&temporary, source, target) < 0 && temporary.temporary[0]);
file_io_temp_discard(&temporary); hash_same(source, before); hash_same(target, before); assert(!remove(target));
#endif
assert(!ma_sha256_file(cipher, hash));
printf("PASS: encrypt/decrypt aliases, active output and alias before publication\n");
}
#ifdef __linux__
static void temp_count(void* arg, const char* path, int64_t size) {
(void)size; if (strstr(path, ".tmp.")) (*(unsigned*)arg)++;
}
/* Реальные файловые операции и CCM; каждый отказ сохраняет source и прежний target до rename. */
static void failures(const char* dir) {
char source[768], cipher[768], target[768];
snprintf(source, sizeof(source), "%s/source", dir); snprintf(cipher, sizeof(cipher), "%s/cipher", dir);
snprintf(target, sizeof(target), "%s/target", dir);
uint8_t key[32] = {1}, id[16] = {2}, hash[32], before[32], previous[32];
struct attachment_info info = {.kind = ATTACHMENT_FILE, .name = "example.bin"};
struct file_io_temp output = {0};
assert(!ma_sha256_file(cipher, hash));
for (int decrypt = 0; decrypt < 2; decrypt++) for (enum fault test = F_OPEN; test <= F_RANDOM; test++) {
if (decrypt && test == F_SEEK) continue;
const char* input = decrypt ? cipher : source;
assert(!ma_sha256_file(input, before));
FILE* file = fopen(target, "wb"); assert(file && fputs("previous", file) >= 0 && !fclose(file));
assert(!ma_sha256_file(target, previous));
struct attachment_info result, unchanged; memset(&result, 0xa5, sizeof(result)); unchanged = result;
char type[32], old_type[32]; memset(type, 0xa5, sizeof(type)); memcpy(old_type, type, sizeof(type));
unsigned fds = count_fds();
fault = test; injected = 0; fault_after = decrypt && test == F_INPUT_CLOSE ? 2 : 1;
int error = decrypt ? encrypted_file_read(input, target, &output, key, 42, id, hash, &result, type)
: encrypted_file_write(input, target, &output, key, 42, id, &info, "application/octet-stream");
int prepared = !error;
if (prepared) error = file_io_temp_publish(&output, input, target);
fault = F_NONE;
assert(error < 0 && injected);
if (decrypt && !prepared) assert(!memcmp(&result, &unchanged, sizeof(result)) && !memcmp(type, old_type, sizeof(type)));
if (test == F_DIRECTORY) assert(!output.temporary[0]);
file_io_temp_discard(&output);
assert(count_fds() == fds); hash_same(input, before);
if (test != F_DIRECTORY) hash_same(target, previous);
unsigned temps = 0; assert(!ma_dir_walk(dir, temp_count, &temps) && !temps);
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS encrypted I/O failure decrypt=%d phase=%d installed=%d", decrypt, test, test == F_DIRECTORY);
}
assert(!remove(target));
}
#endif
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR);
char dir[512] = "/tmp/utun_encrypted_file_XXXXXX"; assert(!test_mkdtemp(dir));
const unsigned sizes[] = { 0, 1, 32767, 32768, 32769, 98321 };
for (unsigned i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) roundtrip(dir, sizes[i]);
aliases(dir);
#ifdef __linux__
failures(dir);
#endif
return 0;
}
+300 -4
View File
@@ -2,9 +2,11 @@
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include "etcp.h"
#include "etcp_api.h"
#include "etcp_session.h"
#include "etcp_loadbalancer.h"
#include "node_conn_direct.h"
#include "topo_node.h"
#include "utun_instance.h"
@@ -13,7 +15,13 @@
#include "test_clock.h"
#include "../lib/mem.h"
#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL %s:%d: %s", __func__, __LINE__, #x); exit(1); } } while (0)
static void diagnostic_flush(void);
static int diagnostic_flushing;
#define CHECK(x) do { if (!(x)) { \
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL %s:%d: %s", __func__, __LINE__, #x); \
if (!diagnostic_flushing) diagnostic_flush(); \
exit(1); \
} } while (0)
#define PATHS 3
#define TRACE_COUNT 131072
#define CMD 253
@@ -78,11 +86,194 @@ static const char* privkeys[2] = {
"38240cb82199e504686507f11f6eaa4f740fde6f0c425c495e49a523019a5d68"
};
/* Диагностика включается UTUN_MULTIPATH_DIAG=path.csv. Срезы и счётчики пишутся в память,
* файл/лог — только после drain каждого стенда. Пики наблюдаются на TX/RX/ACK/выборе пути.
* Интервал по умолчанию 100мс; UTUN_MULTIPATH_DIAG_MS=10..1000 позволяет уточнить короткий участок. */
struct diagnostic_interval {
uint64_t tx_data, fresh_rx, data_packets, retries, ack_only_packets, ack_only_wire, piggy_packets, piggy_bytes;
uint64_t ack_entries, ack_delay_sum, ack_delay_max, ack_only_entries, ack_only_delay_sum, early_ack_entries;
uint64_t ordinary_ack_packets, early_ack_packets; // молодые entries допустимы в пакете, где старейший ACK уже готов
uint64_t control_packets, samples, limited, app_limited;
uint64_t rtt_min, rtt_max, rtt_sum, rtt_samples, searches, window_blocked, pacer_blocked, observations;
uint32_t flight_min, flight_max, cwnd_min, cwnd_max, send_max, ack_max;
int64_t debt_min_us, debt_max_us;
};
struct diagnostic_sample {
const char* phase;
uint64_t time_us, interval_us, wire_bytes, channel_received, channel_lost, channel_dropped, app_bytes, ack_age_us, attempt_age_us;
uint64_t pacer_ready_us;
uint64_t queue_bytes[5];
unsigned queue_count[5], side, path, fresh, retries, pending_bytes, normalizer_bytes, normalizer_waiter, normalizer_flush;
unsigned unacked, budget, flight, attempts, cwnd, pace, mode, cycle, min_rtt, rtt, jitter, rto, bw, queue_cap;
unsigned ack_count, ack_timer, ack_till, urgent_ack, pacer_timer, burst, stalled, rto_streak;
unsigned channel_pending, channel_peak;
unsigned app_suspended, app_resume, fragment_suspended, fragment_resume, waiter_deferred;
unsigned tx_state, tx_scheduled, tx_delayed, tx_running, active_attempts, next_seq, peer_till, local_till;
uint64_t channel_delay_max_us;
int64_t debt_us;
struct diagnostic_interval stats;
};
static FILE* diagnostic_file;
static struct diagnostic_sample diagnostic_rows[16384]; // хватает и для трёх путей/10мс с полным drain
static struct diagnostic_interval diagnostic_stats[2][PATHS];
static struct dummynet_stats diagnostic_previous[2][PATHS];
static unsigned diagnostic_count, diagnostic_active, diagnostic_period_us = 100000;
static uint64_t diagnostic_last_us;
/* Состояние pacer в микросекундах: отрицательное значение — накопленный кредит, положительное — долг. */
static int64_t diagnostic_debt(const struct ETCP_LINK* link) {
return ((int64_t)link->shaper_load_time_tb - (int64_t)get_time_tb()) * 100 + (int64_t)(link->shaper_sub_nanotime / 10000);
}
static void diagnostic_observe(struct diagnostic_interval* d, struct ETCP_LINK* link) {
uint32_t flight = link->inflight_bytes, cwnd = link->inflight_lim_bytes;
int64_t debt = diagnostic_debt(link);
if (!d->observations || flight < d->flight_min) d->flight_min = flight;
if (!d->observations || cwnd < d->cwnd_min) d->cwnd_min = cwnd;
if (!d->observations || debt < d->debt_min_us) d->debt_min_us = debt;
if (!d->observations || debt > d->debt_max_us) d->debt_max_us = debt;
if (flight > d->flight_max) d->flight_max = flight;
if (cwnd > d->cwnd_max) d->cwnd_max = cwnd;
if ((unsigned)link->etcp->input_send_q->count > d->send_max) d->send_max = link->etcp->input_send_q->count;
if ((unsigned)link->ack_q->count > d->ack_max) d->ack_max = link->ack_q->count;
d->observations++;
}
static struct diagnostic_interval* diagnostic_for(struct ETCP_LINK* link) {
if (!diagnostic_active) return NULL;
for (unsigned p = 0; p < path_count; p++)
if (ss_get_port(&link->remote_addr) == dummynet_get_listen_port(channels[p])) {
struct diagnostic_interval* d = &diagnostic_stats[link->etcp->instance == ep[1].inst][p];
diagnostic_observe(d, link); return d;
}
return NULL;
}
/* Общие очереди соединения повторяются в строках его линков; интервальные счётчики относятся к одному пути. */
static void diagnostic_snapshot(const char* phase) {
if (!diagnostic_active) return;
uint64_t now = get_time_us();
if (now == diagnostic_last_us) return;
for (unsigned side = 0; side < 2; side++) for (struct ETCP_LINK* l = ep[side].conn->links; l; l = l->next) {
struct diagnostic_interval* d = diagnostic_for(l);
if (!d) continue;
unsigned p = (unsigned)(d - diagnostic_stats[side]);
CHECK(diagnostic_count < sizeof(diagnostic_rows) / sizeof(diagnostic_rows[0]));
struct diagnostic_sample* s = &diagnostic_rows[diagnostic_count++];
memset(s, 0, sizeof(*s));
struct ETCP_CONN* c = l->etcp;
struct PKTNORM* pn = c->normalizer;
const struct ll_queue* queues[] = {c->send_input_q, c->input_queue, c->input_send_q, c->input_wait_ack, c->recv_q};
s->phase = phase; s->time_us = now - started; s->interval_us = now - diagnostic_last_us; s->side = side; s->path = p;
for (unsigned q = 0; q < 5; q++) { s->queue_count[q] = queues[q]->count; s->queue_bytes[q] = queues[q]->total_bytes; }
for (struct ll_entry* e = c->input_send_q->head; e; e = e->next)
if (((struct INFLIGHT_PACKET*)e)->last_attempt_id) s->retries++; else s->fresh++;
s->normalizer_bytes = pn->data_ptr; s->normalizer_waiter = !!pn->input_waiter_handle.internal;
s->waiter_deferred = !!pn->input_waiter_handle.call_soon_id;
s->normalizer_flush = !!pn->flush_timer; s->pending_bytes = pn->pending ? pn->pending->len - pn->pending_in_ptr : 0;
s->app_suspended = !!c->send_input_q->callback_suspended; s->app_resume = !!c->send_input_q->resume_timeout_id;
s->fragment_suspended = !!c->input_queue->callback_suspended; s->fragment_resume = !!c->input_queue->resume_timeout_id;
s->tx_state = c->tx_state; s->tx_scheduled = c->tx_token != NULL; s->tx_delayed = c->tx_delayed;
s->tx_running = c->tx_running; s->active_attempts = c->active_attempts;
s->next_seq = c->next_tx_id; s->peer_till = c->rx_ack_till; s->local_till = c->last_delivered_id;
s->unacked = c->unacked_bytes; s->budget = c->optimal_inflight;
s->flight = l->inflight_bytes; s->attempts = l->inflight_packets; s->cwnd = l->inflight_lim_bytes; s->pace = l->bbr_pacing_rate;
s->mode = l->bbr->mode; s->cycle = l->bbr->cycle_idx; s->min_rtt = l->bbr->min_rtt_us;
s->rtt = l->data_rtt_us; s->jitter = l->data_jitter_us; s->rto = etcp_tx_rto_us(l);
s->bw = l->bbr->bw_hi[0] > l->bbr->bw_hi[1] ? l->bbr->bw_hi[0] : l->bbr->bw_hi[1];
if (s->bw > l->bbr->bw_lo) s->bw = l->bbr->bw_lo;
s->queue_cap = l->bbr->queue_cwnd_cap; s->ack_count = l->ack_q->count;
s->ack_timer = !!c->ack_resp_timer; s->ack_till = !!c->ack_till_deadline_us; s->urgent_ack = l->ack_reply_pending;
if (l->ack_q->head) s->ack_age_us = now - ((struct ACK_PACKET*)l->ack_q->head)->received_at_us;
if (l->attempts->head && ((struct ETCP_TX_ATTEMPT*)l->attempts->head)->sent_at_us)
s->attempt_age_us = now - ((struct ETCP_TX_ATTEMPT*)l->attempts->head)->sent_at_us;
s->debt_us = diagnostic_debt(l); s->pacer_timer = !!l->shaper_timer; s->burst = l->burst_active;
uint64_t tb = get_time_tb();
s->pacer_ready_us = l->shaper_load_time_tb >= tb + SHAPER_BURST_DELAY_TB ?
(l->shaper_load_time_tb - tb - SHAPER_BURST_DELAY_TB + 1) * 100 : 0;
s->stalled = l->tx_stalled; s->rto_streak = l->data_rto_streak; s->app_bytes = ep[1-side].bytes;
const struct dummynet_stats* ds = dummynet_get_stats(channels[p], side);
struct dummynet_stats* previous = &diagnostic_previous[side][p];
CHECK(ds->sent_bytes >= previous->sent_bytes && ds->recv_bytes >= previous->recv_bytes);
s->wire_bytes = ds->sent_bytes - previous->sent_bytes; s->channel_received = ds->recv_bytes - previous->recv_bytes;
s->channel_lost = ds->lost - previous->lost; s->channel_dropped = ds->dropped - previous->dropped;
s->channel_pending = ds->queue_size; s->channel_peak = ds->queue_max; s->channel_delay_max_us = ds->max_queue_delay_us;
*previous = *ds; s->stats = *d; memset(d, 0, sizeof(*d));
}
diagnostic_last_us = now;
}
/* Вывод после замера: dt_us обязателен для скоростей, особенно в коротком последнем срезе фазы. */
static void diagnostic_flush(void) {
if (!diagnostic_file) return;
diagnostic_flushing = 1;
for (unsigned n = 0; n < diagnostic_count; n++) {
const struct diagnostic_sample* s = &diagnostic_rows[n];
const struct diagnostic_interval* d = &s->stats;
CHECK(fprintf(diagnostic_file, "%s,%x,%s,%llu,%llu,%u,%u", case_name, enabled_mask, s->phase,
(unsigned long long)s->time_us, (unsigned long long)s->interval_us, s->side, s->path) > 0);
for (unsigned q = 0; q < 5; q++)
CHECK(fprintf(diagnostic_file, ",%u,%llu", s->queue_count[q], (unsigned long long)s->queue_bytes[q]) > 0);
CHECK(fprintf(diagnostic_file, ",%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u",
s->fresh, s->retries, s->normalizer_bytes, s->pending_bytes, s->normalizer_waiter, s->normalizer_flush,
s->unacked, s->budget, s->flight, s->attempts, s->cwnd, s->pace, s->mode, s->cycle, s->min_rtt,
s->rtt, s->jitter, s->rto, s->bw, s->queue_cap, s->ack_count, s->ack_timer) > 0);
CHECK(fprintf(diagnostic_file, ",%u,%u,%llu,%llu,%lld,%u,%llu,%u,%u,%u,%u,%u,%llu,%llu,%llu,%llu,%llu,%llu",
s->ack_till, s->urgent_ack, (unsigned long long)s->ack_age_us, (unsigned long long)s->attempt_age_us,
(long long)s->debt_us, s->pacer_timer, (unsigned long long)s->pacer_ready_us, s->burst, s->stalled, s->rto_streak,
s->channel_pending, s->channel_peak, (unsigned long long)s->channel_delay_max_us,
(unsigned long long)s->wire_bytes, (unsigned long long)s->channel_received, (unsigned long long)s->channel_lost,
(unsigned long long)s->channel_dropped, (unsigned long long)s->app_bytes) > 0);
CHECK(fprintf(diagnostic_file, ",%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu",
(unsigned long long)d->tx_data, (unsigned long long)d->fresh_rx, (unsigned long long)d->data_packets,
(unsigned long long)d->retries, (unsigned long long)d->ack_only_packets, (unsigned long long)d->ack_only_wire,
(unsigned long long)d->piggy_packets, (unsigned long long)d->piggy_bytes, (unsigned long long)d->ack_entries,
(unsigned long long)d->ack_delay_sum, (unsigned long long)d->ack_delay_max, (unsigned long long)d->early_ack_entries,
(unsigned long long)d->control_packets, (unsigned long long)d->samples, (unsigned long long)d->limited,
(unsigned long long)d->app_limited, (unsigned long long)d->rtt_min, (unsigned long long)d->rtt_max) > 0);
CHECK(fprintf(diagnostic_file, ",%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%u,%u,%u,%u,%u,%u,%lld,%lld",
(unsigned long long)d->ack_only_entries, (unsigned long long)d->ack_only_delay_sum,
(unsigned long long)d->rtt_sum, (unsigned long long)d->rtt_samples,
(unsigned long long)d->searches, (unsigned long long)d->window_blocked,
(unsigned long long)d->pacer_blocked, (unsigned long long)d->observations, d->flight_min, d->flight_max,
d->cwnd_min, d->cwnd_max, d->send_max, d->ack_max, (long long)d->debt_min_us, (long long)d->debt_max_us) > 0);
CHECK(fprintf(diagnostic_file, ",%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%llu,%llu\n", s->app_suspended, s->app_resume,
s->fragment_suspended, s->fragment_resume, s->waiter_deferred, s->tx_state, s->tx_scheduled, s->tx_delayed,
s->tx_running, s->active_attempts, s->next_seq, s->peer_till, s->local_till,
(unsigned long long)d->ordinary_ack_packets, (unsigned long long)d->early_ack_packets) > 0);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "MPATH %s mask=%x %s t_ms=%llu dt_us=%llu side=%u path=%u "
"Q_app=%u Q_fragment=%u Q_send=%u(new=%u retry=%u) Q_ACK=%u age_us=%llu flight=%u/%u "
"pace_Bps=%u debt_us=%lld ready_us=%llu timer=%u window_blocked_checks=%llu pacer_blocked_checks=%llu "
"channel_pending=%u mode=%u/%u DATA_B=%llu ACKonly=%llu early_ACK_entries=%llu early_ACK_packets=%llu",
case_name, enabled_mask, s->phase, (unsigned long long)(s->time_us/1000), (unsigned long long)s->interval_us,
s->side, s->path, s->queue_count[0], s->queue_count[1], s->queue_count[2], s->fresh, s->retries,
s->ack_count, (unsigned long long)s->ack_age_us, s->flight, s->cwnd, s->pace, (long long)s->debt_us,
(unsigned long long)s->pacer_ready_us, s->pacer_timer,
(unsigned long long)d->window_blocked, (unsigned long long)d->pacer_blocked,
s->channel_pending, s->mode, s->cycle, (unsigned long long)d->tx_data,
(unsigned long long)d->ack_only_packets, (unsigned long long)d->early_ack_entries, (unsigned long long)d->early_ack_packets);
}
CHECK(fflush(diagnostic_file) == 0);
diagnostic_count = 0; diagnostic_flushing = 0;
}
/* Выборочная диагностика модели: один sample на линк/100мс, без packet-level спама всего ETCP. */
void __real_bbr_main(struct bbr*, const struct bbr_rate_sample*, uint32_t*, uint32_t*, uint32_t, uint32_t, int);
void __wrap_bbr_main(struct bbr* model, const struct bbr_rate_sample* sample, uint32_t* cwnd, uint32_t* pacing,
uint32_t mss, uint32_t inflight, int limited) {
__real_bbr_main(model, sample, cwnd, pacing, mss, inflight, limited);
if (diagnostic_active) for (int side = 0; side < 2; side++)
for (struct ETCP_LINK* l = ep[side].conn->links; l; l = l->next) {
if (l->bbr != model) continue;
struct diagnostic_interval* d = diagnostic_for(l);
if (!d) continue;
d->samples++; d->limited += !!limited; d->app_limited += !!sample->is_app_limited;
if (sample->rtt_us) {
if (!d->rtt_min || sample->rtt_us < d->rtt_min) d->rtt_min = sample->rtt_us;
if (sample->rtt_us > d->rtt_max) d->rtt_max = sample->rtt_us;
d->rtt_sum += sample->rtt_us; d->rtt_samples++;
}
}
if (!log_bbr_samples) return;
uint64_t now = get_time_us();
for (int side = 0; side < 2; side++) for (struct ETCP_LINK* l = ep[side].conn ? ep[side].conn->links : NULL; l; l = l->next) {
@@ -103,16 +294,55 @@ void __wrap_bbr_main(struct bbr* model, const struct bbr_rate_sample* sample, ui
}
}
/* Обычный ACK-only может нести timestamp; запрос till и секции измерительной серии относятся к control. */
static int diagnostic_ack_only(const struct ETCP_DGRAM* pkt, unsigned ack_size) {
if (pkt->payload_len || pkt->burst_packet) return 0;
for (unsigned off = ack_size; off < pkt->data_len; off += 5)
if (pkt->data_len - off < 5 || pkt->data[off] != ETCP_SECTION_TIMESTAMP) return 0;
return 1;
}
/* Статистика TX снимается только после принятия send; RX — перед разбором уже расшифрованного пакета. */
static void observe_packet(struct ETCP_DGRAM* pkt, struct ETCP_LINK* link, int sending) {
int side = link->etcp->instance == ep[0].inst ? 0 : 1, path = -1;
for (unsigned p = 0; p < path_count; p++)
if (ss_get_port(&link->remote_addr) == dummynet_get_listen_port(channels[p])) path = p;
if (path >= 0 && !sending) diagnostic_for(link);
if (path >= 0) for (unsigned offset = 0; offset < pkt->data_len;) {
const uint8_t* data = pkt->data + offset;
unsigned remaining = pkt->data_len - offset, len;
switch (data[0]) {
case ETCP_SECTION_ACK: CHECK(remaining >= ETCP_ACK_BASE_SIZE); len = etcp_ack_section_size(data[1]); break;
case ETCP_SECTION_ACK: {
CHECK(remaining >= ETCP_ACK_BASE_SIZE); len = etcp_ack_section_size(data[1]); CHECK(len <= remaining);
struct diagnostic_interval* d = sending ? diagnostic_for(link) : NULL;
if (d) {
int ack_only = diagnostic_ack_only(pkt, len);
if (pkt->payload_len && data[1]) { d->piggy_packets++; d->piggy_bytes += len; }
else if (ack_only) {
d->ack_only_packets++;
d->ack_only_wire += pkt->data_len + ETCP_ENCRYPTED_HDR_SIZE + ETCP_SESSION_HEADER_SIZE +
SC_NONCE_SIZE + SC_CRC32_SIZE + SC_TAG_SIZE;
} else if (!pkt->payload_len) d->control_packets++;
uint32_t oldest_delay = 0;
for (unsigned n = 0; n < data[1]; n++) {
const uint8_t* a = data + ETCP_ACK_BASE_SIZE + n * ETCP_ACK_ENTRY_SIZE;
uint32_t delay = (uint32_t)a[6] | ((uint32_t)a[7] << 8) | ((uint32_t)a[8] << 16) | ((uint32_t)a[9] << 24);
d->ack_entries++; d->ack_delay_sum += delay;
if (delay > oldest_delay) oldest_delay = delay;
if (delay > d->ack_delay_max) d->ack_delay_max = delay;
if (ack_only) {
d->ack_only_entries++; d->ack_only_delay_sum += delay;
if (!link->ack_reply_pending && delay < 2000) d->early_ack_entries++;
}
}
if (ack_only && data[1] && !link->ack_reply_pending) {
d->ordinary_ack_packets++;
int till_due = link->etcp->ack_till_deadline_us && link->etcp->ack_till_deadline_us <= get_time_us();
if (!till_due && oldest_delay < ETCP_ACK_DELAY_US) d->early_ack_packets++;
}
}
break;
}
case ETCP_SECTION_ACK_REQUEST: len = 1; break;
case ETCP_SECTION_TIMESTAMP: len = 5; break;
case ETCP_SECTION_MEAS_TS: len = 9; break;
@@ -127,6 +357,11 @@ static void observe_packet(struct ETCP_DGRAM* pkt, struct ETCP_LINK* link, int s
uint16_t number = (uint16_t)data[5] | ((uint16_t)data[6] << 8);
if (!sending) rx_trigger[side] = seq;
if (sending) {
struct diagnostic_interval* d = diagnostic_for(link);
if (d) {
d->tx_data += remaining - ETCP_PAYLOAD_HEADER_SIZE; d->data_packets++;
if (trace->attempts) d->retries++;
}
if (measuring && fault_start && !side && trace->retry_ready) {
uint64_t wait = now - trace->retry_ready; recovery.retry_count++;
if (wait > recovery.ready_wait_us) { recovery.ready_wait_us = wait; recovery.ready_seq = seq; }
@@ -139,6 +374,7 @@ static void observe_packet(struct ETCP_DGRAM* pkt, struct ETCP_LINK* link, int s
fault_rto_us[path] = etcp_tx_rto_us(link);
if (measuring) traffic[side][path].data += remaining - ETCP_PAYLOAD_HEADER_SIZE;
} else if (!trace->first_rx) {
if (diagnostic_active) diagnostic_stats[1-side][path].fresh_rx += remaining - ETCP_PAYLOAD_HEADER_SIZE;
trace->first_rx = now;
if (measuring) traffic[1-side][path].rx_unique += remaining - ETCP_PAYLOAD_HEADER_SIZE;
if (measuring && fault_start && side && trace->last_number == number && trace->last_path == path) {
@@ -192,12 +428,23 @@ int __wrap_queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry)
}
}
}
return __real_queue_data_put_with_index(q, entry);
int result = __real_queue_data_put_with_index(q, entry);
if (!result && diagnostic_active) for (unsigned side = 0; side < 2; side++)
for (struct ETCP_LINK* l = ep[side].conn->links; l; l = l->next)
if (q == l->ack_q) diagnostic_for(l); // ACK-пик виден до немедленной отправки/извлечения из очереди.
return result;
}
struct ETCP_LINK* __real_etcp_loadbalancer_select_link(struct ETCP_CONN*, uint32_t, const struct INFLIGHT_PACKET*);
struct ETCP_LINK* __wrap_etcp_loadbalancer_select_link(struct ETCP_CONN* conn, uint32_t bytes, const struct INFLIGHT_PACKET* retry) {
struct ETCP_LINK* selected = __real_etcp_loadbalancer_select_link(conn, bytes, retry);
if (bytes && diagnostic_active) for (struct ETCP_LINK* l = conn->links; l; l = l->next) {
struct diagnostic_interval* d = diagnostic_for(l);
if (!d) continue;
d->searches++;
d->window_blocked += bytes > l->inflight_lim_bytes || l->inflight_bytes > l->inflight_lim_bytes - bytes;
d->pacer_blocked += l->shaper_timer != NULL;
}
if (!measuring || !fault_start || conn != ep[0].conn || !retry || !selected || !retry->last_link) return selected;
int previous = -1, chosen = -1;
for (unsigned p = 0; p < path_count; p++) {
@@ -312,6 +559,10 @@ static void setup(const struct profile* settings, unsigned count, unsigned mask)
CHECK(node_conn_direct_open_node(ep[1].inst, peer.node_id, NULL, NULL, &ep[1].owner, &peer, NULL) >= 0);
started = get_time_us();
for (int i = 0; i < 2; i++) ep[i].last_delivery = started;
memset(diagnostic_stats, 0, sizeof(diagnostic_stats)); memset(diagnostic_previous, 0, sizeof(diagnostic_previous));
diagnostic_count = 0; diagnostic_last_us = started; diagnostic_active = diagnostic_file != NULL;
if (diagnostic_active) for (unsigned side = 0; side < 2; side++) for (unsigned p = 0; p < path_count; p++)
if (channels[p]) diagnostic_previous[side][p] = *dummynet_get_stats(channels[p], side);
}
/* Ограниченная очередь источника: latency не скрывается за неограниченным накоплением тестовых сообщений. */
@@ -331,6 +582,7 @@ static void produce(unsigned side) {
}
static void snapshot(const char* phase) {
diagnostic_snapshot(phase);
uint64_t now = get_time_us();
for (int side = 0; side < 2; side++) {
struct ETCP_CONN* conn = ep[side].conn;
@@ -361,10 +613,13 @@ static void run_for(unsigned ms, int sending, const char* phase) {
if (sending) { produce(0); if (bidirectional) produce(1); }
test_clock_poll(ua, 10);
for (int i = 0; i < 2; i++) CHECK(ep[i].conn->state != 2 && !ep[i].conn->reinit_pending);
if (get_time_us() >= next_snapshot) { snapshot(phase); next_snapshot = get_time_us() + 100000; }
if (get_time_us() >= next_snapshot) {
snapshot(phase); next_snapshot = get_time_us() + (diagnostic_file ? diagnostic_period_us : 100000);
}
if (!sending && ep[0].sent == ep[1].received && ep[1].sent == ep[0].received &&
!ep[0].conn->unacked_bytes && !ep[1].conn->unacked_bytes) break;
}
diagnostic_snapshot(phase);
}
static void begin_measure(void) {
@@ -375,6 +630,7 @@ static void begin_measure(void) {
memset(ep[i].latency_hist, 0, sizeof(ep[i].latency_hist));
}
for (unsigned p = 0; p < path_count; p++) if (channels[p]) dummynet_reset_stats(channels[p], -1);
memset(diagnostic_previous, 0, sizeof(diagnostic_previous));
}
static void expect(int passed, const char* requirement, uint64_t actual, uint64_t limit) {
@@ -396,6 +652,7 @@ static uint64_t percentile(const struct endpoint* endpoint, unsigned percent) {
static void teardown(void) {
measuring = 0;
diagnostic_active = 0; diagnostic_flush();
// Уничтожаем и активные delay/HOLD пакеты: эмуляторы не должны пережить свои endpoints.
for (unsigned p = 0; p < path_count; p++) { dummynet_destroy(channels[p]); channels[p] = NULL; }
for (int i = 0; i < 2; i++) node_conn_direct_close(ep[i].owner);
@@ -570,6 +827,41 @@ int main(int argc, char** argv) {
if (output) { csv = fopen(output, "w"); CHECK(csv); fprintf(csv,
"case,mask,phase,time_ms,direction,path,rtt_tb,pacing_kbps,inflight,cwnd,conn_rtt_tb,jitter,send_q,local_recv_q,wire_bytes,"
"unique_bytes,retries,channel_q,app_bytes,max_gap_us,dest_recv_q,needed_seq,queue_drops\n"); }
const char* detail = getenv("UTUN_MULTIPATH_DIAG");
if (detail) {
const char* period = getenv("UTUN_MULTIPATH_DIAG_MS");
if (period) {
char* end = NULL; errno = 0;
unsigned long ms = strtoul(period, &end, 10);
if (errno || end == period || *end || ms < 10 || ms > 1000) {
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "Invalid UTUN_MULTIPATH_DIAG_MS='%s': expected 10..1000", period); return 1;
}
diagnostic_period_us = ms * 1000;
}
diagnostic_file = fopen(detail, "w");
if (!diagnostic_file) {
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "Cannot open multipath diagnostic file '%s': errno=%d", detail, errno); return 1;
}
CHECK(fprintf(diagnostic_file,
"case,mask,phase,time_us,dt_us,side,path,app_count,app_bytes,fragment_count,fragment_bytes,send_count,send_bytes,"
"wait_ack_count,wait_ack_bytes,recv_count,recv_bytes,send_fresh,send_retries,normalizer_bytes,pending_bytes,"
"normalizer_waiter,normalizer_flush,unacked_bytes,admission_budget,flight_bytes,attempts,cwnd_bytes,pacing_Bps,"
"bbr_mode,bbr_cycle,min_rtt_us,srtt_us,jitter_us,rto_us,bw_q16,queue_cap_bytes,ack_count,ack_timer,"
"till_pending,urgent_reply,ack_oldest_us,attempt_oldest_us,pacer_debt_us,pacer_timer,pacer_ready_us,burst,stalled,"
"rto_streak,channel_pending,channel_peak_since_reset,channel_delay_max_since_reset_us,wire_sent_bytes,"
"channel_received_bytes,channel_lost,channel_dropped,app_delivered_bytes,data_tx_bytes,fresh_rx_bytes,"
"data_packets,retries,ack_only_packets,ack_only_wire_bytes,ack_piggy_packets,ack_piggy_bytes,ack_entries,"
"ack_delay_sum_us,ack_delay_max_us,early_ordinary_ack_entries,control_packets,bbr_samples,cwnd_limited_samples,"
"app_limited_samples,rtt_min_us,rtt_max_us,ack_only_entries,ack_only_delay_sum_us,rtt_sum_us,rtt_samples,"
"selection_checks,selection_window_blocked,selection_pacer_blocked,observed_states,flight_min,flight_max,"
"cwnd_min,cwnd_max,send_peak_observed,ack_peak_observed,debt_min_us,debt_max_us,app_callback_suspended,"
"app_resume_scheduled,fragment_callback_suspended,fragment_resume_scheduled,normalizer_waiter_deferred,"
"tx_state,tx_scheduled,tx_delayed,tx_running,active_attempts,next_tx_seq,peer_ack_till,local_delivered_till,"
"ordinary_ack_only_packets,early_ordinary_ack_packets\n") > 0);
debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Multipath diagnostics: period_us=%u file=%s; snapshots buffered until drain",
diagnostic_period_us, detail);
}
struct profile matrix[][3] = {
{{20,8000},{20,8000}}, {{20,2000},{20,8000}}, {{20,8000},{200,8000}},
{{20,2000},{200,8000}}, {{20,8000},{200,2000}}, {{20,2000},{80,8000},{200,2000}}, {{20,2000},{80,8000}}
@@ -608,6 +900,10 @@ int main(int argc, char** argv) {
}
CHECK(selected && argc <= 2);
if (csv) CHECK(fclose(csv) == 0);
if (diagnostic_file) {
FILE* completed = diagnostic_file; diagnostic_file = NULL;
CHECK(fclose(completed) == 0);
}
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Multipath completed: failures=%u", failures);
test_clock_finish(); socket_platform_cleanup(); return failures ? 1 : 0;
}
+182
View File
@@ -33,13 +33,21 @@ static uint32_t last_mss;
static unsigned fail_rx_message_at, fail_tx_inflight, fail_normalizer_timer, fail_normalizer_waiter, fail_session_timer_at;
static unsigned no_alloc, unexpected_alloc, fail_rng, session_sends, received, receive_action;
static uint8_t received_values[8];
static unsigned ack_clock_active, capture_ack_timer, fail_ack_timer;
static uint64_t ack_now_us;
static timeout_callback_t captured_ack_callback;
static void* captured_ack_arg;
/* Фиксируем только TB на время синхронной проверки pacer; uasync при этих часах не опрашивается. */
uint64_t __real_get_time_tb(void);
uint64_t __wrap_get_time_tb(void) {
if (ack_clock_active) return ack_now_us / 100;
return pacer_clock_active ? pacer_now_tb : __real_get_time_tb();
}
uint64_t __real_get_time_us(void);
uint64_t __wrap_get_time_us(void) { return ack_clock_active ? ack_now_us : __real_get_time_us(); }
void* __real_uasync_call_soon(struct UASYNC*, void*, timeout_callback_t);
void* __wrap_uasync_call_soon(struct UASYNC* loop, void* arg, timeout_callback_t callback) {
if (fail_normalizer_timer && conn && arg == conn->normalizer) { fail_normalizer_timer--; return NULL; }
@@ -52,6 +60,8 @@ void* __wrap_uasync_call_soon(struct UASYNC* loop, void* arg, timeout_callback_t
void* __real_uasync_set_timeout(struct UASYNC*, int, void*, timeout_callback_t, const char*);
void* __wrap_uasync_set_timeout(struct UASYNC* loop, int delay, void* arg, timeout_callback_t callback, const char* name) {
if (capture_ack_timer && !strcmp(name, "etcp_ack_resp")) { captured_ack_callback = callback; captured_ack_arg = arg; }
if (fail_ack_timer && !strcmp(name, "etcp_ack_resp")) { fail_ack_timer--; return NULL; }
if (capture_shaper && name && !strcmp(name, "etcp_shaper")) {
shaper_arm_count++; captured_shaper_delay = delay;
captured_shaper_callback = callback; captured_shaper_arg = arg;
@@ -152,6 +162,8 @@ static void setup(void) {
fail_rx_message_at = fail_tx_inflight = fail_normalizer_timer = fail_normalizer_waiter = fail_session_timer_at = 0;
no_alloc = unexpected_alloc = fail_rng = session_sends = received = receive_action = 0;
memset(received_values, 0, sizeof(received_values));
ack_clock_active = capture_ack_timer = fail_ack_timer = 0;
captured_ack_callback = NULL; captured_ack_arg = NULL;
last_sent_link = NULL; last_sent_request = last_sent_payload = 0;
ua = uasync_create(); CHECK(ua);
inst.ua = ua; inst.node_id = 1; inst.config = &cfg; cfg.global.bbr_max_cwnd = INFLIGHT_LIM_MAX;
@@ -175,6 +187,7 @@ static void setup(void) {
}
static void teardown(void) {
ack_clock_active = 0;
check_accounting();
etcp_connection_close(conn); conn = NULL;
uasync_poll(ua, 0);
@@ -204,6 +217,12 @@ static struct ETCP_DGRAM* request(void) {
return pkt;
}
/* Wire/window-тесты моделируют уже ожидающий ACK, не меняя часы и не запуская sender через uasync. */
static void mature_acks(struct ETCP_LINK* link) {
for (struct ll_entry* e = link->ack_q->head; e; e = e->next)
((struct ACK_PACKET*)e)->received_at_us = get_time_us() - ETCP_ACK_DELAY_US;
}
static void fill(struct ETCP_LINK* target, unsigned count, unsigned len) {
int up_a = a->link_status, up_b = b->link_status;
a->link_status = target == a; b->link_status = target == b;
@@ -788,6 +807,7 @@ static void retry_waits_for_alternative(void) {
CHECK(!sends && packet->attempt_count == 1 && !packet->active_attempts && !a->retrans_timer && !b->retrans_timer);
CHECK(packet->state == INFLIGHT_STATE_WAIT_SEND && packet->last_link == a && ua->timer_alloc_count == allocated);
CHECK(etcp_tx_queue_ack(a, 99, etcp_tx_attempt_id(99, 1)) == 0);
mature_acks(a);
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == a && !pkt->payload_len && !pkt->attempt);
memory_pool_free(inst.pkt_pool, pkt);
a->burst_active = 1; a->burst_count = 1;
@@ -1124,6 +1144,162 @@ static void independent_attempt_acks(void) {
check_accounting(); teardown();
}
/* Посторонний wake не снимает задержку ACK; точные границы проверяются без опроса uasync при ручных часах. */
static void ack_absolute_deadline(void) {
setup(); conn->got_initial_pkt = 1;
ack_now_us = get_time_us(); ack_clock_active = capture_ack_timer = 1;
receive_fragment(a, 2, 1); CHECK(a->ack_q->count == 1 && conn->ack_resp_timer);
etcp_conn_wake_sender(conn);
struct ETCP_DGRAM* pkt = request();
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "ACK deadline: age_us=0 queued=%d sent_early=%u", a->ack_q->count, pkt != NULL);
CHECK(!pkt && a->ack_q->count == 1);
ack_now_us += 1999; CHECK(!request() && a->ack_q->count == 1);
ack_now_us++; pkt = request(); CHECK(pkt && pkt->link == a && pkt->data[1] == 1);
memory_pool_free(inst.pkt_pool, pkt); CHECK(!a->ack_q->count && !conn->ack_resp_timer);
teardown();
}
static void ack_piggyback_deadlines(void) {
setup(); conn->got_initial_pkt = 1;
ack_now_us = get_time_us(); ack_clock_active = capture_ack_timer = 1;
uint64_t begin = ack_now_us;
receive_fragment(a, 2, 1); ack_now_us += 1000; receive_fragment(b, 3, 1);
CHECK(conn->ack_resp_deadline_us == begin + ETCP_ACK_DELAY_US);
enqueue(100);
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == a && pkt->payload_len && pkt->data[1] == 1);
memory_pool_free(inst.pkt_pool, pkt); CHECK(!a->ack_q->count && b->ack_q->count == 1);
CHECK(conn->ack_resp_timer && conn->ack_resp_deadline_us == begin + 3000);
ack_now_us = begin + 2000; CHECK(!request());
ack_now_us = begin + 2999; CHECK(!request());
ack_now_us++; pkt = request(); CHECK(pkt && pkt->link == b && !pkt->payload_len && pkt->data[1] == 1);
memory_pool_free(inst.pkt_pool, pkt); CHECK(!conn->ack_resp_timer && !conn->ack_resp_deadline_us);
ack_all(); teardown();
}
/* Частичный piggyback меняет голову: прежний deadline больше не делает молодой ACK готовым. */
static void ack_partial_piggyback(void) {
setup(); conn->got_initial_pkt = 1; b->link_status = 0;
ack_now_us = get_time_us(); ack_clock_active = capture_ack_timer = 1;
uint64_t begin = ack_now_us;
receive_fragment(a, 2, 1); ack_now_us += 500; receive_fragment(a, 3, 1);
unsigned max_body = a->mtu - WIRE_OVERHEAD(AF_INET);
enqueue(max_body - ETCP_ACK_BASE_SIZE - ETCP_PAYLOAD_HEADER_SIZE - ETCP_ACK_ENTRY_SIZE - ETCP_ACK_RX_POINT_SIZE);
ack_now_us = begin + 1999;
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->payload_len && pkt->data[1] == 1 && a->ack_q->count == 1);
memory_pool_free(inst.pkt_pool, pkt); CHECK(conn->ack_resp_deadline_us == begin + 2500);
timeout_callback_t callback = captured_ack_callback; void* arg = captured_ack_arg;
ack_now_us = begin + 2000; uasync_cancel_timeout(ua, conn->ack_resp_timer); callback(arg);
CHECK(conn->ack_resp_timer && conn->ack_resp_deadline_us == begin + 2500 && !request());
ack_now_us = begin + 2100; receive_fragment(a, 4, 1);
CHECK(conn->ack_resp_deadline_us == begin + 2500);
ack_now_us = begin + 2500; pkt = request(); CHECK(pkt && !pkt->payload_len && pkt->data[1] == 2);
memory_pool_free(inst.pkt_pool, pkt); CHECK(!a->ack_q->count && !conn->ack_resp_timer);
ack_all(); teardown();
}
/* Новый RX между timer и отложенным sender не запускает для старой головы очередные 2мс. */
static void ack_timer_interleaving(void) {
setup(); conn->got_initial_pkt = 1;
ack_now_us = get_time_us(); ack_clock_active = capture_ack_timer = 1;
uint64_t begin = ack_now_us;
receive_fragment(a, 2, 1);
timeout_callback_t callback = captured_ack_callback; void* arg = captured_ack_arg;
ack_now_us += ETCP_ACK_DELAY_US;
uasync_cancel_timeout(ua, conn->ack_resp_timer); callback(arg);
CHECK(!conn->ack_resp_timer && conn->tx_token);
receive_fragment(a, 3, 1); CHECK(!conn->ack_resp_timer && conn->tx_token);
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->data[1] == 2 && !pkt->payload_len);
const uint8_t* entry = pkt->data + ETCP_ACK_BASE_SIZE;
uint32_t delay = (uint32_t)entry[6] | ((uint32_t)entry[7] << 8) | ((uint32_t)entry[8] << 16) | ((uint32_t)entry[9] << 24);
CHECK(delay == ETCP_ACK_DELAY_US && ack_now_us == begin + ETCP_ACK_DELAY_US);
memory_pool_free(inst.pkt_pool, pkt); teardown();
}
static void ack_urgent_and_oom(void) {
for (unsigned mode = 0; mode < 2; mode++) {
setup(); conn->got_initial_pkt = 1;
ack_now_us = get_time_us(); ack_clock_active = 1;
fail_ack_timer = mode;
receive_fragment(a, 2, 1);
if (!mode) receive_till(a, 0, 1);
CHECK(a->ack_reply_pending && !fail_ack_timer);
CHECK(((struct ACK_PACKET*)a->ack_q->head)->received_at_us == ack_now_us);
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->data[1] == 1 && !pkt->payload_len);
memory_pool_free(inst.pkt_pool, pkt);
CHECK(!a->ack_reply_pending && !conn->ack_resp_timer && !conn->ack_resp_deadline_us);
teardown();
}
}
static void ack_partial_frame_and_lifecycle(void) {
setup(); ack_now_us = get_time_us(); ack_clock_active = 1;
for (unsigned n = 0; n < 200; n++) CHECK(etcp_tx_queue_ack(a, 100+n, etcp_tx_attempt_id(100+n, 1)) == 0);
ack_now_us += ETCP_ACK_DELAY_US;
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->data[1] && a->ack_q->count);
memory_pool_free(inst.pkt_pool, pkt); CHECK(!conn->ack_resp_timer);
pkt = request(); CHECK(pkt && pkt->data[1] && !a->ack_q->count);
memory_pool_free(inst.pkt_pool, pkt); teardown();
setup(); ack_now_us = get_time_us(); ack_clock_active = 1;
conn->ack_till_deadline_us = ack_now_us + ETCP_ACK_DELAY_US; conn->last_delivered_id = 17;
CHECK(!request() && conn->ack_resp_timer);
ack_now_us += ETCP_ACK_DELAY_US;
pkt = request(); CHECK(pkt && !pkt->data[1] && pkt->data[2] == 17);
memory_pool_free(inst.pkt_pool, pkt); CHECK(!conn->ack_till_deadline_us && !conn->ack_resp_timer);
CHECK(etcp_tx_queue_ack(a, 99, etcp_tx_attempt_id(99, 1)) == 0); CHECK(!request() && conn->ack_resp_timer);
etcp_conn_reset(conn); CHECK(!conn->ack_resp_timer && !conn->ack_resp_deadline_us && !conn->ack_till_deadline_us);
teardown();
}
static void ack_sender_failure(void) {
setup(); conn->got_initial_pkt = 1; CHECK(etcp_conn_ref_take(conn) == 0);
ack_now_us = get_time_us(); ack_clock_active = capture_ack_timer = 1;
receive_fragment(a, 2, 1);
timeout_callback_t callback = captured_ack_callback; void* arg = captured_ack_arg;
ack_now_us += ETCP_ACK_DELAY_US; fail_tx_timer = 1;
uasync_cancel_timeout(ua, conn->ack_resp_timer); callback(arg);
CHECK(!fail_tx_timer && conn->close_requested && !conn->ack_resp_timer);
ack_clock_active = 0; uasync_drain_immediate(ua);
CHECK(conn->state == 2 && !conn->links && !conn->ack_resp_deadline_us && !conn->ack_till_deadline_us);
etcp_conn_ref_free(conn); teardown();
}
/* DOWN не мешает срокам других путей; UP возвращает старые ACK сразу. Удаление головы не оставляет чужой deadline. */
static void ack_link_lifecycle(void) {
for (unsigned remove = 0; remove < 2; remove++) {
setup(); conn->got_initial_pkt = 1;
ack_now_us = get_time_us(); ack_clock_active = 1;
uint64_t begin = ack_now_us;
receive_fragment(a, 2, 1); ack_now_us += 1000; receive_fragment(b, 3, 1);
if (remove) { etcp_link_close(a); a = NULL; }
else a->link_status = 0;
ack_now_us = begin + 2000; CHECK(!request() && conn->ack_resp_deadline_us == begin + 3000);
ack_now_us = begin + 3000;
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == b && pkt->data[1] == 1);
memory_pool_free(inst.pkt_pool, pkt); CHECK(!conn->ack_resp_timer);
if (!remove) {
a->link_status = 1; etcp_conn_wake_sender(conn);
pkt = request(); CHECK(pkt && pkt->link == a && pkt->data[1] == 1);
memory_pool_free(inst.pkt_pool, pkt);
}
teardown();
}
}
/* Готовый DATA другого пути не должен обходить уже наступивший срок ACK. */
static void ack_due_other_link(void) {
setup(); conn->got_initial_pkt = 1;
ack_now_us = get_time_us(); ack_clock_active = 1;
receive_fragment(b, 2, 1); b->inflight_lim_bytes = 1;
ack_now_us += ETCP_ACK_DELAY_US;
struct INFLIGHT_PACKET* fresh = enqueue(100);
struct ETCP_DGRAM* pkt = request();
CHECK(pkt && pkt->link == b && !pkt->payload_len && pkt->data[1] == 1 && fresh->state == INFLIGHT_STATE_WAIT_SEND);
memory_pool_free(inst.pkt_pool, pkt);
pkt = request(); CHECK(pkt && pkt->link == a && pkt->payload_len == 100);
memory_pool_free(inst.pkt_pool, pkt); ack_all(); teardown();
}
static void receive_admission_and_path_acks(void) {
setup(); conn->got_initial_pkt = 1;
fail_rx_data = 1; receive_fragment(a, 2, 1);
@@ -1250,6 +1426,7 @@ static void receiver_point_wire(void) {
CHECK(a->rx_data_bytes == 2 && a->ack_q->count == 1 && conn->recv_q->count == 1);
a->rx_data_bytes = UINT64_C(0xfedcba9876543210); a->rx_data_time_us = UINT64_C(0x0011223344556677);
const uint8_t expected[] = {0x10,0x32,0x54,0x76,0x98,0xba,0xdc,0xfe,0x77,0x66,0x55,0x44,0x33,0x22,0x11,0x00};
mature_acks(a);
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->data[1] == 1 && !pkt->payload_len);
CHECK(pkt->data_len == etcp_ack_section_size(1) && !memcmp(pkt->data+ETCP_ACK_BASE_SIZE+ETCP_ACK_ENTRY_SIZE,expected,16));
memory_pool_free(inst.pkt_pool,pkt);
@@ -1266,12 +1443,14 @@ static void receiver_point_wire(void) {
else CHECK(!pkt->data[1] && a->ack_q->count == 1);
memory_pool_free(inst.pkt_pool,pkt);
if (a->ack_q->count) {
mature_acks(a);
pkt = request(); CHECK(pkt && pkt->data[1] == 1 && !pkt->payload_len);
memory_pool_free(inst.pkt_pool,pkt);
}
ack(seq);
}
for (unsigned i = 100; i < 300; i++) receive_fragment(a,i,1000+i);
mature_acks(a);
unsigned count = (max_body-ETCP_ACK_BASE_SIZE-ETCP_ACK_RX_POINT_SIZE)/ETCP_ACK_ENTRY_SIZE;
pkt = request(); CHECK(pkt && pkt->data[1] == count && pkt->data_len <= max_body && a->ack_q->count == 200-(int)count);
memory_pool_free(inst.pkt_pool,pkt);
@@ -1472,6 +1651,7 @@ static void compact_attempt_identity(void) {
conn->got_initial_pkt = 1;
receive_fragment(a,2,1); receive_fragment(a,3,1); receive_fragment(a,2,1);
CHECK(a->ack_q->count == 2 && a->rx_data_bytes == 3);
mature_acks(a);
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && !pkt->payload_len && pkt->data[1] == 2);
const uint8_t entry1[] = {2,0,0,0,1,0}, entry2[] = {3,0,0,0,1,0};
CHECK(!memcmp(pkt->data+ETCP_ACK_BASE_SIZE,entry1,sizeof(entry1)));
@@ -1545,6 +1725,8 @@ int main(void) {
retry_waits_for_alternative, retry_alternative_lifecycle,
send_queue_scan, send_queue_input_admission,
retry_full_round_history, retry_history_slot_reuse, fast_stall_progress, stalled_retry_selection, input_admission_callbacks,
ack_absolute_deadline, ack_piggyback_deadlines, ack_partial_piggyback, ack_timer_interleaving,
ack_urgent_and_oom, ack_partial_frame_and_lifecycle, ack_sender_failure, ack_link_lifecycle, ack_due_other_link,
receive_admission_and_path_acks, peer_window_queries, pacing_continuity, pacing_data_units, attempt_memory_limits,
normalizer_rx_oom, normalizer_tx_oom, normalizer_callback_abort, reset_without_allocation, restart_allocation_failures,
receiver_bandwidth_bounds, receiver_point_lifecycle, receiver_point_wire, receiver_point_truncated,
+47 -6
View File
@@ -19,7 +19,8 @@ static char root[256], source[512], destination[512];
static unsigned visits;
#ifdef __linux__
enum fault { NONE, READ, WRITE, FLUSH, SOURCE_CLOSE, OUTPUT_CLOSE, FDOPEN, PUBLISH, OPEN_DIR, READ_DIR, CLOSE_DIR, SEEK, THREAD };
enum fault { NONE, READ, WRITE, FLUSH, SOURCE_CLOSE, OUTPUT_CLOSE, FDOPEN, PUBLISH, OPEN_DIR, READ_DIR, CLOSE_DIR, SEEK, THREAD,
SYNC_FILE, SYNC_DIRECTORY };
static enum fault fault;
static int injected, read_failed, rng_after = -1, allocation_after = -1;
static FILE *input_file, *output_file;
@@ -35,6 +36,7 @@ extern size_t __real_fwrite(const void*, size_t, size_t, FILE*);
extern int __real_ferror(FILE*), __real_fflush(FILE*), __real_fclose(FILE*);
extern int __real_rename(const char*, const char*), __real_RAND_bytes(unsigned char*, int);
extern int __real_fseeko(FILE*, off_t, int);
extern int __real_fsync(int);
extern DIR *__real_opendir(const char*);
extern struct dirent *__real_readdir(DIR*);
extern int __real_closedir(DIR*);
@@ -84,6 +86,11 @@ int __wrap_fclose(FILE *file) {
return result;
}
int __wrap_rename(const char *from, const char *to) { return inject(PUBLISH) ? -1 : __real_rename(from, to); }
int __wrap_fsync(int fd) {
struct stat data; assert(!fstat(fd, &data));
if (inject(S_ISDIR(data.st_mode) ? SYNC_DIRECTORY : SYNC_FILE)) return -1;
return __real_fsync(fd);
}
int __wrap_RAND_bytes(unsigned char *out, int size) {
if (rng_after == 0) { injected = 1; return 0; }
if (rng_after > 0) rng_after--;
@@ -169,7 +176,7 @@ static void temporary_tests(void) {
struct file_io_temp outputs[3] = {0};
char paths[3][FILE_IO_PATH_MAX], stale[FILE_IO_PATH_MAX];
for (unsigned i = 0; i < 3; i++) {
assert(!file_io_temp_open(&outputs[i], destination));
assert(!file_io_temp_open(&outputs[i], destination, 0));
snprintf(paths[i], sizeof(paths[i]), "%s", outputs[i].temporary);
assert(file_io_remove_orphan(paths[i]) == 0);
assert(fputs("owned", outputs[i].file) >= 0 && !file_io_temp_finish(&outputs[i]));
@@ -187,10 +194,10 @@ static void temporary_tests(void) {
#ifdef __linux__
struct file_io_temp failed = {0};
fault = FDOPEN; injected = 0;
assert(file_io_temp_open(&failed, destination) == -1 && injected); fault = NONE;
assert(file_io_temp_open(&failed, destination, 0) == -1 && injected); fault = NONE;
assert(failed.temporary[0] && file_io_remove_orphan(failed.temporary) == 0);
file_io_temp_discard(&failed);
assert(!file_io_temp_open(&failed, destination) && !file_io_temp_finish(&failed));
assert(!file_io_temp_open(&failed, destination, 0) && !file_io_temp_finish(&failed));
fault = PUBLISH; injected = 0;
assert(file_io_temp_publish(&failed, source, destination) == -1 && injected); fault = NONE;
assert(file_io_remove_orphan(failed.temporary) == 0); file_io_temp_discard(&failed);
@@ -198,6 +205,40 @@ static void temporary_tests(void) {
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS active temp ownership: open/finished, multiple generations, publish/discard and failures");
}
/* Ошибка до rename сохраняет прежний файл; отказ directory sync уже не отменяет замену. */
static void durable_tests(void) {
struct file_io_temp output = {0};
write_text(destination, "previous");
assert(!file_io_temp_open(&output, destination, FILE_IO_DURABLE));
assert(fputs("durable", output.file) >= 0 && !file_io_temp_finish(&output));
check_text(destination, "previous"); assert(file_io_remove_orphan(output.temporary) == 0);
assert(!file_io_temp_publish(&output, source, destination) && !output.temporary[0]);
file_io_temp_discard(&output); check_text(destination, "durable");
assert(!file_io_sync_file(destination) && !file_io_sync_directory("relative-name"));
#ifndef _WIN32
assert(!file_io_sync_directory("/file"));
#endif
#ifdef __linux__
assert(!file_io_temp_open(&output, destination, FILE_IO_DURABLE));
assert(fputs("failed", output.file) >= 0);
fault = SYNC_FILE; injected = 0;
assert(file_io_temp_finish(&output) == -1 && injected && !output.file && output.temporary[0]); fault = NONE;
file_io_temp_discard(&output); check_text(destination, "durable");
assert(!file_io_temp_open(&output, destination, FILE_IO_DURABLE));
assert(fputs("installed", output.file) >= 0 && !file_io_temp_finish(&output));
fault = SYNC_DIRECTORY; injected = 0;
assert(file_io_temp_publish(&output, source, destination) == -1 && injected && !output.temporary[0]); fault = NONE;
file_io_temp_discard(&output); check_text(destination, "installed");
assert(!file_io_temp_open(&output, destination, 0));
assert(fputs("ordinary", output.file) >= 0);
fault = SYNC_FILE; injected = 0;
assert(!file_io_temp_finish(&output) && !injected); fault = SYNC_DIRECTORY;
assert(!file_io_temp_publish(&output, source, destination) && !injected); fault = NONE;
file_io_temp_discard(&output); check_text(destination, "ordinary");
#endif
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS durable temp: sync-before-close, rename commit point, directory failure and ordinary mode");
}
static void crypto_tests(void) {
uint8_t output[64], zero[64] = {0};
memset(output, 0xa5, sizeof(output));
@@ -291,7 +332,7 @@ static void registration_tests(struct UASYNC *ua) {
struct temp_job { struct file_io_temp output; pthread_barrier_t barrier; int publish; };
static void* temp_worker(void* arg) {
struct temp_job* job = arg;
assert(!file_io_temp_open(&job->output, destination));
assert(!file_io_temp_open(&job->output, destination, 0));
assert(fputs("worker", job->output.file) >= 0 && !file_io_temp_finish(&job->output));
pthread_barrier_wait(&job->barrier); pthread_barrier_wait(&job->barrier);
if (job->publish) assert(!file_io_temp_publish(&job->output, source, destination));
@@ -384,7 +425,7 @@ int main(void) {
#endif
assert(!utun_mkdir(root, 0700)); snprintf(source, sizeof(source), "%s/source", root);
snprintf(destination, sizeof(destination), "%s/destination", root);
copy_tests(); temporary_tests(); crypto_tests(); size_and_walk_tests();
copy_tests(); temporary_tests(); durable_tests(); crypto_tests(); size_and_walk_tests();
struct UASYNC *ua = uasync_create(); assert(ua);
registration_tests(ua);
#ifdef __linux__
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""Проверить CSV UTUN_MULTIPATH_DIAG и построить автономный HTML с графиками и исходными срезами."""
import argparse
import csv
import json
from pathlib import Path
def read_samples(path):
"""Проверить схему, учёт очередей и интервалы; неправильный CSV не превращать в правдоподобный график."""
with path.open(newline="", encoding="utf-8") as source:
reader = csv.DictReader(source)
if not reader.fieldnames or len(set(reader.fieldnames)) != len(reader.fieldnames):
raise ValueError("отсутствуют или повторяются заголовки CSV")
rows = []
previous = {}
for line, raw in enumerate(reader, 2):
if None in raw or any(value is None for value in raw.values()):
raise ValueError(f"строка {line}: число значений не соответствует заголовку")
row = {key: value if key in ("case", "mask", "phase") else int(value) for key, value in raw.items()}
key = (row["case"], row["mask"], row["side"], row["path"])
valid = row["dt_us"] > 0 and row["unacked_bytes"] == row["send_bytes"] + row["wait_ack_bytes"]
valid &= row["send_count"] == row["send_fresh"] + row["send_retries"]
valid &= row["flight_min"] <= row["flight_bytes"] <= row["flight_max"]
valid &= row["cwnd_min"] <= row["cwnd_bytes"] <= row["cwnd_max"]
valid &= row["early_ordinary_ack_entries"] <= row["ack_only_entries"] <= row["ack_entries"]
valid &= row["early_ordinary_ack_packets"] <= row["ordinary_ack_only_packets"] <= row["ack_only_packets"]
valid &= row["retries"] <= row["data_packets"] and row["rtt_samples"] <= row["bbr_samples"]
if key in previous:
valid &= row["time_us"] - previous[key] == row["dt_us"]
if not valid:
raise ValueError(f"строка {line}: нарушен учёт очередей, счётчиков или времени")
previous[key] = row["time_us"]
rows.append(row)
if not rows:
raise ValueError("CSV не содержит срезов")
return rows
PAGE = r'''<!doctype html>
<html lang="ru"><meta charset="utf-8"><title>ETCP: очереди и pacing</title>
<style>
body{font:14px system-ui;margin:24px;background:#f5f7fa;color:#182438;max-width:1400px}
h1{font-size:23px}label{display:inline-block;margin:0 16px 12px 0}select,input{font:inherit;padding:4px}
canvas{display:block;width:100%;height:190px;background:white;border:1px solid #dae0e9;border-radius:6px}
section{margin:18px 0}h2{font-size:16px;margin:8px 0}small{color:#526176}.legend{margin:5px 0}
.legend span{margin-right:16px}table{border-collapse:collapse;background:white;width:100%}
td,th{text-align:left;padding:4px 12px;border-bottom:1px solid #e6eaf0}#raw{columns:2;column-gap:30px}
#raw table{break-inside:avoid}#status{font-weight:600;margin:8px 0}button{font:inherit;padding:5px 12px}
</style>
<h1>ETCP: динамика очередей, DATA и ACK</h1>
<p>Источник: <b id="source"></b>. Данные проверены. Наведите курсор на график для просмотра всех полей среза.</p>
<label>Стенд <select id="fixture"></select></label><label>Направление <select id="side"></select></label>
<label>Канал <select id="path"></select></label><label>Фаза <select id="phase"></select></label>
<label>От, с <input id="from" type="number" step="0.1" size="6"></label>
<label>До, с <input id="to" type="number" step="0.1" size="6"></label><button id="reset">Весь интервал</button>
<p><small>Очереди приложения/normalizer/send/wait_ack общие для соединения. Flight, pacer, ACK и канал — для выбранного пути.
Блокировки — число проверок выбора, а не время ожидания. Оранжевый фон: BBR PROBE_RTT.
Пики очереди и задержки эмулятора накоплены с последнего сброса его статистики.</small></p>
<div id="status"></div><div id="charts"></div><h2>Исходный срез</h2><div id="raw"></div>
<script>
const rows=__ROWS__, sourceName=__SOURCE__;
const $=id=>document.getElementById(id), colors=['#1768ac','#dc4e41','#2b8c59','#8856a7','#d08800','#444'];
const rate=(r,k)=>r[k]*8000/r.dt_us, kb=r=>r/1024;
const definitions=[
['Очереди: пакеты',[['Приложение',r=>r.app_count],['Фрагменты',r=>r.fragment_count],['send',r=>r.send_count],
['wait_ack',r=>r.wait_ack_count],['ACK',r=>r.ack_count]]],
['Окно канала: КиБ',[['inflight',r=>kb(r.flight_bytes)],['cwnd',r=>kb(r.cwnd_bytes)],
['min flight',r=>kb(r.flight_min)],['max flight',r=>kb(r.flight_max)]]],
['Скорость: кбит/с',[['pacing DATA',r=>r.pacing_Bps*8/1000],['DATA TX',r=>rate(r,'data_tx_bytes')],
['новые DATA RX',r=>rate(r,'fresh_rx_bytes')],['UDP из эмулятора',r=>rate(r,'wire_sent_bytes')]]],
['Pacer: долг и срок готовности, мкс',[['Долг (+) / кредит (−)',r=>r.pacer_debt_us],
['До готовности',r=>r.pacer_ready_us],['min debt',r=>r.debt_min_us],['max debt',r=>r.debt_max_us],['Допуск 1мс',r=>1000]]],
['ACK: накладные расходы, кбит/с',[['ACK-only',r=>rate(r,'ack_only_wire_bytes')],
['ACK внутри DATA',r=>rate(r,'ack_piggy_bytes')]]],
['Задержка ACK и RTT: мс',[['ACK-only mean',r=>r.ack_only_entries?r.ack_only_delay_sum_us/r.ack_only_entries/1000:0],
['ACK max',r=>r.ack_delay_max_us/1000],['SRTT',r=>r.srtt_us/1000],['min RTT',r=>r.min_rtt_us===4294967295?0:r.min_rtt_us/1000]]],
['Наблюдения блокировки выбора: проверки за срез',[['Окно',r=>r.selection_window_blocked],
['Pacer',r=>r.selection_pacer_blocked],['ACK-пакеты раньше дедлайна',r=>r.early_ordinary_ack_packets]]],
['Эмулятор: ожидающие пакеты',[['Все ожидающие (delay + сериализация + HOLD)',r=>r.channel_pending]]]
];
let current=[], selectedTime=null;
function options(id,values){
const select=$(id),old=select.value;select.replaceChildren();
for(const [value,label] of values){const o=document.createElement('option');o.value=value;o.textContent=label;select.append(o);}
if(values.some(v=>String(v[0])===old))select.value=old;
}
function fixtureRows(){return rows.filter(r=>r.case+'/'+r.mask===$('fixture').value);}
function updateChoices(){
const fixture=fixtureRows();options('side',[...new Set(fixture.map(r=>r.side))].sort().map(v=>[v,v?'B → A':'A → B']));
const side=fixture.filter(r=>r.side===Number($('side').value));
options('path',[...new Set(side.map(r=>r.path))].sort().map(v=>[v,String(v)]));
options('phase',[['*','Все'],...[...new Set(side.map(r=>r.phase))].map(v=>[v,v])]);
updateRange();
}
function filtered(){return fixtureRows().filter(r=>r.side===Number($('side').value)&&r.path===Number($('path').value)&&
($('phase').value==='*'||r.phase===$('phase').value));}
function updateRange(){
const v=filtered();$('from').value=v.length?v[0].time_us/1e6:0;$('to').value=v.length?v.at(-1).time_us/1e6:1;draw();
}
function showRaw(row){
if(!row)return;$('status').textContent=`t=${(row.time_us/1e6).toFixed(6)} с; dt=${row.dt_us} мкс; `+
`${row.phase}; BBR=${row.bbr_mode}/${row.bbr_cycle}; ${current.length} срезов в диапазоне`;
const table=document.createElement('table');
for(const [key,value] of Object.entries(row)){const tr=table.insertRow();tr.insertCell().textContent=key;tr.insertCell().textContent=value;}
$('raw').replaceChildren(table);
}
function draw(){
const lo=Number($('from').value)*1e6,hi=Number($('to').value)*1e6;
current=filtered().filter(r=>r.time_us>=lo&&r.time_us<=hi);$('charts').replaceChildren();
if(!current.length){$('status').textContent='В выбранном диапазоне нет срезов';$('raw').replaceChildren();return;}
const first=current[0].time_us,last=Math.max(first+1,current.at(-1).time_us);
for(const [title,series] of definitions){
const section=document.createElement('section'),heading=document.createElement('h2');heading.textContent=title;section.append(heading);
const legend=document.createElement('div');legend.className='legend';
series.forEach(([label],i)=>{const span=document.createElement('span');span.style.color=colors[i];span.textContent='● '+label;legend.append(span);});
section.append(legend);const canvas=document.createElement('canvas');section.append(canvas);$('charts').append(section);
const ratio=window.devicePixelRatio||1,w=canvas.clientWidth,h=190;canvas.width=w*ratio;canvas.height=h*ratio;
const c=canvas.getContext('2d');c.scale(ratio,ratio);const left=65,right=w-18,top=12,bottom=h-26;
let ymin=0,ymax=1;for(const r of current)for(const [,value]of series){const v=value(r);ymin=Math.min(ymin,v);ymax=Math.max(ymax,v);}
if(ymax===ymin)ymax=ymin+1;const x=t=>left+(t-first)/(last-first)*(right-left),y=v=>bottom-(v-ymin)/(ymax-ymin)*(bottom-top);
for(let i=0;i<current.length;i++)if(current[i].bbr_mode===3){
c.fillStyle='#fff0d4';c.fillRect(x(current[i].time_us),top,Math.max(1,x(current[i+1]?.time_us||last)-x(current[i].time_us)),bottom-top);
}
c.font='11px system-ui';c.fillStyle='#526176';c.strokeStyle='#e3e8ef';
for(let n=0;n<=4;n++){const value=ymin+(ymax-ymin)*n/4,yy=y(value);c.beginPath();c.moveTo(left,yy);c.lineTo(right,yy);c.stroke();
c.fillText(value.toFixed(Math.abs(value)<10?2:0),3,yy+4);const t=first+(last-first)*n/4;c.fillText((t/1e6).toFixed(2),x(t)-12,h-6);}
series.forEach(([,value],i)=>{c.strokeStyle=colors[i];c.lineWidth=i<2?1.8:1;c.beginPath();
current.forEach((r,n)=>n?c.lineTo(x(r.time_us),y(value(r))):c.moveTo(x(r.time_us),y(value(r))));c.stroke();});
canvas.onmousemove=event=>{const t=first+(event.offsetX-left)/(right-left)*(last-first);
const row=current.reduce((a,b)=>Math.abs(a.time_us-t)<Math.abs(b.time_us-t)?a:b);selectedTime=row.time_us;showRaw(row);};
}
showRaw(current.reduce((a,b)=>Math.abs(a.time_us-(selectedTime??first))<Math.abs(b.time_us-(selectedTime??first))?a:b));
}
$('source').textContent=sourceName;
options('fixture',[...new Set(rows.map(r=>r.case+'/'+r.mask))].map(v=>[v,v+' (mask)']));
$('fixture').onchange=updateChoices;$('side').onchange=updateChoices;$('path').onchange=updateRange;$('phase').onchange=updateRange;
$('from').onchange=draw;$('to').onchange=draw;$('reset').onclick=updateRange;window.onresize=draw;updateChoices();
</script></html>'''
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("csv", type=Path)
parser.add_argument("html", type=Path)
args = parser.parse_args()
try:
rows = read_samples(args.csv)
page = PAGE.replace("__ROWS__", json.dumps(rows, ensure_ascii=False).replace("<", "\\u003c"))
page = page.replace("__SOURCE__", json.dumps(args.csv.name, ensure_ascii=False).replace("<", "\\u003c"))
args.html.write_text(page, encoding="utf-8")
except (OSError, ValueError, KeyError) as error:
parser.exit(1, f"Ошибка отчёта ETCP: {error}\n")
print(f"Проверено {len(rows)} срезов; отчёт: {args.html}")
if __name__ == "__main__":
main()