Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
80558f6686
30 files changed
+1808
-201
No files matched your search
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 или закрытия старых нестабильных тестов.
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 до первого измерения
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user