diff --git a/doc/etcp_multipath_plan.txt b/doc/etcp_multipath_plan.txt index bf4fc425..98c0fcaa 100644 --- a/doc/etcp_multipath_plan.txt +++ b/doc/etcp_multipath_plan.txt @@ -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 и уведомление о реальном прогрессе/готовности должны diff --git a/doc/etcp_protocol.txt b/doc/etcp_protocol.txt index 7c061f14..b05302fd 100644 --- a/doc/etcp_protocol.txt +++ b/doc/etcp_protocol.txt @@ -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 diff --git a/doc/node_snapshot.md b/doc/node_snapshot.md index 5ed661bb..7a3fd930 100644 --- a/doc/node_snapshot.md +++ b/doc/node_snapshot.md @@ -6,6 +6,14 @@ заменяются вместе. Timestamp входит в подпись. Групповые пути и подсети остаются в `TOPO_GROUP_NODE` и не являются частью общей записи узла. +В NODEINFO секции подсетей присутствуют только при `TOPO_FLAG_SEND_SUBNETS`. +Без флага оба счётчика подсетей равны нулю; противоречащий этому заголовок отклоняется. +Parser проверяет точную длину пакета и проходит все присутствующие секции независимо +от запрошенных `out_*`: например, `out_subnets=NULL` отключает создание объектов подсетей, +но сохраняет проверку префиксов и правильное положение следующего hop-list. +Выходы выдаются только после полного успеха; при ошибке они обнулены, частичные объекты +освобождены. Подпись и допустимость пути проверяются после структурного разбора. + Получатель проверяет подпись до изменения реестра или маршрутов. Более свежая запись заменяет старую целиком, включая пустые списки адресов. Старые записи игнорируются; разные подписанные записи с одинаковым timestamp — конфликт. @@ -13,6 +21,12 @@ 8-битный `ver` сохраняется как диагностический счётчик; свежесть определяется только timestamp. +Локальный узел группы готовится до удаления прежнего `local_node`: сравниваются сами адреса +и префиксы подсетей, включая замену при прежнем количестве. При отказе памяти, подписи или +конфликте IPv4-маршрутов прежние узел и маршруты сохраняются. Подсети CHAT не публикуются. +`topo_group_update_my_nodeinfo` возвращает число IPv4-подсетей, для CHAT — 0, при ошибке — -1; +рассылку подготовленного NODEINFO выполняет вызывающий. + Живая телефонная занятость передаётся без изменения структуры NODEINFO: в существующем `flags` бит `0x04` означает известное состояние, `0x08` — занятость звонком. Это групповые флаги `TOPO_GROUP_NODE.phone_status`, как флаг рации `0x02`; @@ -50,6 +64,8 @@ snapshot загрузка использует его. Локальная пуб Уникальность определяется endpoint и видом адреса, а не отображаемым классом NAT. Старый производный формат пересоздаётся; bootstrap-загрузка без подписанного snapshot не подтверждает NAT-профиль. +Такая загрузка требует хотя бы одного адреса; отказ SQL/памяти освобождает все частичные списки +и возвращает NULL. Наличие валидного snapshot с пустым списком адресов, напротив, допустимо. При удалении старых `nat_evidence`/`nat_checked` адреса и REALITY сохраняются для bootstrap, старые профили сбрасываются, сериализованные snapshots инвалидируются. Максимальные timestamp snapshots сохраняются, чтобы новая локальная публикация diff --git a/doc/tasks.md b/doc/tasks.md index ac47aeb1..4c30356a 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -43,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мс. diff --git a/src/routing_layer/address_profile.h b/src/routing_layer/address_profile.h index 92de64ba..77897c54 100644 --- a/src/routing_layer/address_profile.h +++ b/src/routing_layer/address_profile.h @@ -22,11 +22,12 @@ enum address_inbound { enum address_profile_source { ADDRESS_SOURCE_NONE, ADDRESS_SOURCE_ETCP, ADDRESS_SOURCE_STUN }; struct address_profile { - uint8_t mapping; - uint8_t inbound; + uint8_t mapping; /* enum address_mapping: UNKNOWN, без трансляции, EIM или зависимый mapping. */ + uint8_t inbound; /* enum address_inbound: UNKNOWN, OPEN либо нужен исходящий контакт. */ uint8_t source; /* ETCP — измерение рабочего endpoint; STUN — предварительный EIM? с неизвестным входом. */ }; +/* Постоянные строки для диагностики; неизвестное значение возвращает INVALID. */ static inline const char *address_mapping_name(uint8_t value) { static const char *const names[] = {"UNKNOWN", "UNCHANGED", "EIM", "DEPENDENT"}; return value <= ADDRESS_MAP_DEPENDENT ? names[value] : "INVALID"; @@ -42,6 +43,8 @@ static inline const char *address_profile_source_name(uint8_t value) { return value <= ADDRESS_SOURCE_STUN ? names[value] : "INVALID"; } +/* 1 — поля входят в enum и при source=NONE оба свойства UNKNOWN; 0 — иначе/NULL. + * Проверяет представление, а не достоверность измерения или сочетания свойств. */ static inline int address_profile_valid(const struct address_profile *profile) { return profile && profile->mapping <= ADDRESS_MAP_DEPENDENT && profile->inbound <= ADDRESS_IN_CONTACT && profile->source <= ADDRESS_SOURCE_STUN && diff --git a/src/routing_layer/route6_lib.h b/src/routing_layer/route6_lib.h index 8b40ab99..d421a338 100644 --- a/src/routing_layer/route6_lib.h +++ b/src/routing_layer/route6_lib.h @@ -39,10 +39,17 @@ struct ROUTE6_TABLE { struct ll_queue *queue; // indexed by node_id for delete }; +/* Создать таблицу в ua (его время жизни длиннее таблицы); NULL при ошибке. */ struct ROUTE6_TABLE *route6_table_create(struct UASYNC *ua); +/* Освободить таблицу и записи, сохраняя заимствованные узлы; NULL допустим. */ void route6_table_destroy(struct ROUTE6_TABLE *table); +/* Вставить IPv6-подсети node. Вложенные префиксы допустимы; дубликаты radix пропускает. + * true означает завершённый обход, даже если ничего не добавлено. false — неверный вход/нет подсетей/OOM; + * при OOM ранее вставленные подсети остаются. Адреса и /0..128 должен проверить вызывающий. */ bool route6_insert(struct ROUTE6_TABLE *table, struct TOPO_GROUP_NODE *node); +/* Удалить все записи с node->node_id (не по равенству указателей); NULL table/node допустимы. */ void route6_delete(struct ROUTE6_TABLE *table, struct TOPO_GROUP_NODE *node); +/* Найти наиболее длинный подходящий префикс; addr — ненулевой указатель на 16 сетевых байт. NULL — нет маршрута. */ const struct ROUTE6_DATA *route6_lookup(struct ROUTE6_TABLE *table, const uint8_t addr[16]); diff --git a/src/routing_layer/route_connectivity.h b/src/routing_layer/route_connectivity.h index 0a112a1c..2e7d3693 100644 --- a/src/routing_layer/route_connectivity.h +++ b/src/routing_layer/route_connectivity.h @@ -16,18 +16,23 @@ extern "C" { struct UTUN_INSTANCE; struct TOPO_GROUP; -// Запускает зондирование связности ко всем адресам удалённого узла +/* Запустить асинхронные пробы известных адресов nq из registry; group — его группа. + * UDP: до 8 сокетов, серия CONN_PROBE_COUNT с каждого до первого успешного сокета; + * TCP: один STCP ping с REALITY-параметрами при наличии. RTT — 0.1 мс, UINT16_MAX неизвестен. + * До 16 уникальных IP:port на семейство. Собственный/спящий узел и текущие пробы пропускаются; + * NAT/standby могут отложить старт. Результат — в nq->connectivity, завершение уведомляет topo callback. */ void route_connectivity_probe_node(struct UTUN_INSTANCE* instance, struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq); -// Отменяет все pending пробы для узла (при удалении / withdraw) +/* Отменить UDP/TCP-пробы и отложенный старт, освободить контексты; NULL допустим. + * Сбрасывает probe_status/pending_count, сохранённые RTT/status остаются. */ void route_connectivity_cancel_node(struct TOPO_GROUP_NODE* nq); -// Отменяет все pending пробы для всех узлов (при destroy) +// Отменяет pending пробы всех узлов во всех группах instance (при destroy) void route_connectivity_cancel_all(struct UTUN_INSTANCE* instance); // Устанавливает таймаут одного probe-пинга (по умолчанию CONN_PROBE_TIMEOUT_MS). -// Используется тестами для ускорения probe-циклов. +// Используется тестами для ускорения probe-циклов; настройка общая для процесса, 0 игнорируется. void route_connectivity_set_probe_timeout_ms(uint16_t ms); diff --git a/src/routing_layer/route_connectivity_doc.md b/src/routing_layer/route_connectivity_doc.md index ca292186..e3fc2055 100644 --- a/src/routing_layer/route_connectivity_doc.md +++ b/src/routing_layer/route_connectivity_doc.md @@ -1,64 +1,42 @@ -# route_connectivity — зондирование прямой связности с удалённым узлом +# route_connectivity — прямые пробы адресов узла -## 1. Назначение -Проверяет прямую IP-достижимость всех адресов удалённого узла через `etcp_send_ping_to_socket()`. -Для каждого адреса перебирает локальные сокеты-кандидаты (до 8), с каждого делает серию из 3 пингов -(CONN_PROBE_COUNT=3, CONN_PROBE_TIMEOUT_MS=1000). Результаты (reachable/unreachable, min_rtt) -сохраняются в `TOPO_CONNECTIVITY` узла и используются `conn_mgr` для выбора оптимального пути. +`route_connectivity_probe_node(instance, group, nq)` берёт общую запись узла из +registry и запускает асинхронные пробы. Собственный узел, пир в SLEEP-фазе и уже +идущие/отложенные пробы пропускаются. NAT-окно сохраняет незнакомые IP для проверки +входящих; Android standby откладывает старт до активного интервала. -В отличие от `route_ping` (пинг через BGP-посредника), здесь пинги идут напрямую с локальных сокетов -на адреса удалённого узла — без промежуточных узлов. +Для каждого семейства перебираются до 16 уникальных IP:port. Нулевые адреса/порты +пропускаются. Дедупликация выполняется по IP:port, без учёта типа адреса/протокола. -## 2. Как пользоваться -```c -// Запустить зондирование узла (вызывается conn_mgr при обнаружении нового узла): -route_connectivity_probe_node(instance, nodeq); +UDP проверяется с обычных локальных сокетов того же семейства (до 8). +IPv4-кандидаты фильтруются через `sock_match` в порядке списка сокетов; предпочтений +по /24 или рейтинга DIRECT/EIM здесь нет. Для IPv6 сначала идут сокеты той же /64, +затем остальные IPv6. TCP-сокеты и `probe_only` для UDP исключены. +С каждого UDP-сокета идёт последовательная серия из `CONN_PROBE_COUNT=3` ping; +достаточно одного ответа в серии, чтобы завершить этот адрес успешно. Следующий +сокет пробуется, если текущая серия не дала успеха. RTT — минимум успешной серии, +а не обязательный минимум по всем доступным сокетам. -// Отменить зондирование (при удалении узла / withdraw): -route_connectivity_cancel_node(instance, nodeq); +TCP-адрес проверяется одним `etcp_send_tcp_ping` через STCP-клиент, независимо +от набора UDP-сокетов. Используются REALITY-параметры из записи узла; адрес с +REALITY-флагом без параметров пропускается. Проба не создаёт READY-сессию группы. +Таймаут по умолчанию 1000 мс; `route_connectivity_set_probe_timeout_ms(ms)` меняет +его для новых контекстов всего процесса, 0 игнорируется. -// Отменить все зондирования (при destroy): -route_connectivity_cancel_all(instance); -``` +Результаты находятся в `nq->connectivity`: `probe_status`, `pending_count`, +`interface/nat/real_status`, соответствующие `*_min_rtt` и `*_probe_time`. +RTT и время — timebase (0.1 мс), UINT16_MAX означает неизвестный RTT. +INTERFACE обновляет также исторические поля REAL; отдельного третьего адреса +для этого не требуется. Успех одного адреса не затирается неудачей другого. +Завершение последней pending-пробы устанавливает DONE, обновляет БД и вызывает +`topo_fire_nodeinfo_cbk`. Если пригодных адресов/сокетов нет, DONE означает только +конец обхода: UNKNOWN не превращается автоматически в UNREACHABLE. -Результаты доступны через `nodeq->connectivity`: -- `probe_status` — `PROBE_STATUS_NONE` / `PROBE_STATUS_IN_PROGRESS` / `PROBE_STATUS_DONE` -- `interface_status/nat_status/real_status` — `PROBE_RESULT_UNKNOWN` / `REACHABLE` / `UNREACHABLE` -- `interface_min_rtt/nat_min_rtt/real_min_rtt` — минимальный RTT по всем сокетам (0.1ms) -- `pending_count` — счётчик незавершённых серий (когда 0 → PROBE_STATUS_DONE) +`route_connectivity_cancel_node(nq)` отменяет все UDP/TCP-пробы и отложенный старт, +освобождает контексты и сбрасывает probe_status/pending_count. Последние результаты +RTT/status сохраняются. Отмена нужна до удаления узла, группы или рабочих сокетов. +`route_connectivity_cancel_all(instance)` обходит все группы, не только UTUN. +Все вызовы выполняются в потоке ядра; group/nq заимствованы до завершения или отмены. -Ключевой нюанс: зондирование пропускает собственный узел (`nq->node->node_id == instance->node_id`). -Если зондирование уже в процессе, повторный вызов игнорируется. - -## 3. API - -### Внутренние структуры -| Структура | Назначение | -|-----------|------------| -| `conn_probe_ctx` | Контекст одной серии проб: `addr_type` (INTERFACE/NAT/REAL), `target_addr`, `candidate_sockets[8]`, `best_across_sockets` (min RTT по всем опробованным сокетам), `min_rtt` (в текущей серии), счётчики | - -### Подбор сокетов-кандидатов (`conn_match_candidate_sockets`) -Для каждого адреса узла подбирает локальные сокеты в порядке предпочтения: -1. **INTERFACE-адрес**: PRIVATE/LOCAL сокеты в той же /24 подсети -2. **NAT/REAL-адрес**: DIRECT > EIM > PUBLIC/UNKNOWN сокеты -3. Fallback: остальные подходящие сокеты, затем все IPv4 - -Для IPv6: предпочитает сокеты в той же /64 подсети. - -### Функции -| Функция | Назначение | -|---------|------------| -| `route_connectivity_probe_node(instance, nq)` | Перебрать все v4/v6 адреса узла, для каждого подобрать сокеты-кандидаты, запустить серии пингов по 3. Заполняет `nq->connectivity` | -| `route_connectivity_cancel_node(instance, nq)` | Сбросить `probe_status=NONE`, `pending_count=0`, занулить `nq` в ctx (чтобы callback не обновлял статистику) | -| `route_connectivity_cancel_all(instance)` | Сбросить состояние всех узлов в default-группе | - -### Логика завершения -- Серия из 3 пингов с одного сокета: если все 3 потеряны → переход к следующему сокету -- Если хоть 1 пинг успешен → серия считается успешной для данного адреса, обновляется `*_status=REACHABLE` и `*_min_rtt` -- Когда `pending_count` доходит до 0 → `probe_status = PROBE_STATUS_DONE` - -### Зависимости -- `src/etcp_connections.h` — `etcp_send_ping_to_socket()` (прямой пинг) -- `src/topo_node.h` — `TOPO_GROUP_NODE`, `TOPO_CONNECTIVITY`, `TOPO_ADDR4`, `TOPO_ADDR6` -- `src/topo_group.h` — `topo_groups_get_default()` (для cancel_all) -- `lib/u_async.h` — get_time_tb() для отметок времени +`route_ping` также выполняет прямые UDP-пробы, но для измерения наблюдаемого endpoint; +выбор наблюдателей/помощников и вычисление NAT-профиля находятся в `nat_detection`. diff --git a/src/routing_layer/route_crypto.h b/src/routing_layer/route_crypto.h index e2b34123..43080422 100644 --- a/src/routing_layer/route_crypto.h +++ b/src/routing_layer/route_crypto.h @@ -9,6 +9,9 @@ // Порядок кодирования: encrypt-then-sign (подпись заголовка и payload, включая шифртекст). // Список пройденных узлов добавляет/снимает etcp_router снаружи этого формата. // При ошибке декодирования (плохая подпись / не удалось расшифровать) пакет дропается. +// Все вызовы — в потоке ядра; inst и его registry/ключи должны быть готовы. +// Вход заимствован и не изменяется; при успехе *out принадлежит вызывающему (u_free). +// При ошибке out/out_len не устанавливаются. Буфер выделяется и при mode=0/отсутствии crypto-флагов. #ifndef ROUTE_CRYPTO_H #define ROUTE_CRYPTO_H @@ -28,6 +31,7 @@ struct UTUN_INSTANCE; // Кодировать пакет: base = [SVC_ROUTE_HDR][plaintext payload]. // mode — битовая маска ROUTE_CRYPTO_SIGN / ROUTE_CRYPTO_ENCRYPT. +// peer_node_id — получатель ECDH; hdr.src_node_id должен быть локальным, исходные crypto-флаги очищены. // Возвращает новый буфер (u_malloc) с финальным пакетом. 0 = успех, <0 = ошибка. int route_crypto_encode(struct UTUN_INSTANCE* inst, uint64_t peer_node_id, const uint8_t* base, size_t base_len, int mode, @@ -36,6 +40,7 @@ int route_crypto_encode(struct UTUN_INSTANCE* inst, uint64_t peer_node_id, // Декодировать пакет: wire = [SVC_ROUTE_HDR][payload][sig?]. // Проверяет подпись и/или расшифровывает. Через *rx_flags сообщает, что было на wire // (ROUTE_CRYPTO_SIGN / ROUTE_CRYPTO_ENCRYPT). Возвращает [SVC_ROUTE_HDR][plaintext] с очищенными флагами. +// Ключ отправителя берётся по hdr.src_node_id из registry. rx_flags обязателен; может быть заполнен и при ошибке. // 0 = успех, <0 = ошибка (пакет дропается). int route_crypto_decode(struct UTUN_INSTANCE* inst, const uint8_t* wire, size_t wire_len, diff --git a/src/routing_layer/route_lib.c b/src/routing_layer/route_lib.c index 90b3898b..dfcabb66 100644 --- a/src/routing_layer/route_lib.c +++ b/src/routing_layer/route_lib.c @@ -21,22 +21,12 @@ static uint32_t prefix_to_mask(uint8_t prefix) { } static bool routes_overlap(uint32_t net1, uint8_t pre1, uint32_t net2, uint8_t pre2) { - if (pre1 == pre2 && net1 == net2) return true; - return false; -} - -static bool check_route_overlap_in_table(uint32_t network, uint8_t prefix, - const struct ROUTE_ENTRY *entries, size_t count) { - for (size_t i = 0; i < count; i++) { - if (routes_overlap(network, prefix, entries[i].network, entries[i].prefix_length)) { - return true; - } - } - return false; + uint32_t mask = prefix_to_mask(pre1 < pre2 ? pre1 : pre2); + return (net1 & mask) == (net2 & mask); } // ============================================================================ -// Поиск (LPM) +// Поиск по непересекающимся диапазонам // ============================================================================ static int binary_search_insert_pos(struct ROUTE_ENTRY *entries, size_t count, @@ -56,51 +46,19 @@ static int binary_search_insert_pos(struct ROUTE_ENTRY *entries, size_t count, return left; } -static struct ROUTE_ENTRY* binary_search_lpm(struct ROUTE_TABLE *table, uint32_t dest_ip) { - if (table->count == 0) return NULL; - +/* В отсортированной таблице без пересечений подходящий диапазон может быть только один. */ +static struct ROUTE_ENTRY* binary_search_route(struct ROUTE_TABLE *table, uint32_t dest_ip) { int left = 0; int right = (int)table->count - 1; - struct ROUTE_ENTRY *best = NULL; - uint8_t best_prefix = 0; - while (left <= right) { int mid = left + (right - left) / 2; struct ROUTE_ENTRY *entry = &table->entries[mid]; uint32_t mask = prefix_to_mask(entry->prefix_length); - - if ((dest_ip & mask) == (entry->network & mask)) { - if (entry->prefix_length > best_prefix) { - best_prefix = entry->prefix_length; - best = entry; - } - if (mid + 1 < (int)table->count) { - struct ROUTE_ENTRY *next = &table->entries[mid + 1]; - uint32_t next_mask = prefix_to_mask(next->prefix_length); - if ((dest_ip & next_mask) == (next->network & next_mask)) { - left = mid + 1; - } else { - break; - } - } else { - break; - } - } else if (dest_ip > entry->network) { - left = mid + 1; - } else { - right = mid - 1; - } + if ((dest_ip & mask) == entry->network) return entry; + if (dest_ip > entry->network) left = mid + 1; + else right = mid - 1; } - - // fallback проверка предыдущей записи - if (!best && left > 0) { - struct ROUTE_ENTRY *entry = &table->entries[left - 1]; - uint32_t mask = prefix_to_mask(entry->prefix_length); - if ((dest_ip & mask) == (entry->network & mask)) { - best = entry; - } - } - return best; + return NULL; } // ============================================================================ @@ -160,8 +118,25 @@ bool route_insert(struct ROUTE_TABLE *table, struct TOPO_GROUP_NODE *node) { for (const struct TOPO_SUBNET4* s = sub; s; s = s->next) { uint32_t network; memcpy(&network, s->addr, 4); network = ntohl(network); - if (check_route_overlap_in_table(network, s->prefix_length, table->entries, table->count)) { - DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "overlap detected for %s/%d", ip_to_string(network).a, s->prefix_length); + if (s->prefix_length > 32) { + DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "reject node=%016llx subnet=%s/%u: invalid IPv4 prefix", + (unsigned long long)node->node_id, ip_to_string(network).a, s->prefix_length); + return false; + } + for (size_t i = 0; i < table->count; i++) { + const struct ROUTE_ENTRY *e = &table->entries[i]; + if (!routes_overlap(network, s->prefix_length, e->network, e->prefix_length)) continue; + DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "reject node=%016llx subnet=%s/%u: overlaps node=%016llx subnet=%s/%u", + (unsigned long long)node->node_id, ip_to_string(network).a, s->prefix_length, + (unsigned long long)e->v_node_info->node_id, ip_to_string(e->network).a, e->prefix_length); + return false; + } + for (const struct TOPO_SUBNET4 *previous = sub; previous != s; previous = previous->next) { + uint32_t other; memcpy(&other, previous->addr, 4); other = ntohl(other); + if (!routes_overlap(network, s->prefix_length, other, previous->prefix_length)) continue; + DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "reject node=%016llx subnet=%s/%u: overlaps own subnet=%s/%u", + (unsigned long long)node->node_id, ip_to_string(network).a, s->prefix_length, + ip_to_string(other).a, previous->prefix_length); return false; } } @@ -176,6 +151,7 @@ bool route_insert(struct ROUTE_TABLE *table, struct TOPO_GROUP_NODE *node) { for (const struct TOPO_SUBNET4* s = sub; s; s = s->next) { uint32_t network; memcpy(&network, s->addr, 4); network = ntohl(network); + network &= prefix_to_mask(s->prefix_length); int pos = binary_search_insert_pos(table->entries, table->count, network, s->prefix_length); if (pos < (int)table->count) memmove(&table->entries[pos + 1], &table->entries[pos], (table->count - pos) * sizeof(struct ROUTE_ENTRY)); struct ROUTE_ENTRY* e = &table->entries[pos]; @@ -210,7 +186,7 @@ struct ROUTE_ENTRY* route_lookup(struct ROUTE_TABLE *table, uint32_t dest_ip) { DEBUG_TRACE(DEBUG_CATEGORY_ROUTING, "dest_ip=%s", ip_to_string(dest_ip).a); - struct ROUTE_ENTRY *result = binary_search_lpm(table, dest_ip); + struct ROUTE_ENTRY *result = binary_search_route(table, dest_ip); if (result) { table->stats.routes_lookup_hits++; diff --git a/src/routing_layer/route_lib.h b/src/routing_layer/route_lib.h index a9a2a57d..1a811a1b 100644 --- a/src/routing_layer/route_lib.h +++ b/src/routing_layer/route_lib.h @@ -1,5 +1,7 @@ /* route_lib — локальная IPv4-таблица подсетей UTUN, отсортированный массив без пересечения маршрутов. - * create → insert/add_local_subnet → lookup → delete/destroy; все операции в одном uasync-потоке. + * create → insert → lookup → delete/destroy; все операции в одном uasync-потоке. + * Пересечения и дубликаты запрещены, в том числе внутри одного набора подсетей; /0 занимает всю таблицу. + * Вставка нормализует адреса по маске; при отказе таблица и её статистика не меняются. * network/dest_ip/parse_subnet — host order; только is_local_subnet принимает network order. * TOPO_GROUP_NODE заимствован: удалить его маршруты до освобождения узла. * lookup возвращает элемент массива, действительный до следующего изменения таблицы. */ @@ -39,24 +41,24 @@ typedef enum { struct ROUTE_ENTRY { uint32_t network; // Сетевой адрес в host order uint8_t prefix_length; // Длина префикса подсети - struct TOPO_GROUP_NODE* v_node_info; // узел владелец этих маршрутов. null если - локальный маршрут. + struct TOPO_GROUP_NODE* v_node_info; // заимствованный владелец; локальный маршрут тоже содержит узел }; /** * @brief Таблица маршрутизации * * Структура представляет собой таблицу маршрутизации, содержащую записи маршрутов, подсети и статистику. - * маршруты не должны пересекаться. т.е. адрес 192.168.1.1/30 нельзя добавить если есть 192.168.1.2/32 или 192.168.1.1/24 + * Подсети не пересекаются: например, 192.168.1.0/24 исключает 192.168.1.0/30 и 192.168.1.2/32. */ struct ROUTE_TABLE { struct ROUTE_ENTRY *entries; /**< Массив записей маршрутов */ size_t count; /**< Текущее количество записей */ size_t capacity; /**< Максимальная емкость таблицы */ uint64_t my_node_id; /**< ID локального узла */ - uint32_t *dynamic_subnets; /**< Динамические подсети (массив пар: сеть, префикс) */ - size_t dynamic_subnet_count; /**< Количество динамических подсетей */ - uint32_t *local_subnets; /**< Локальные подсети (массив пар: сеть, префикс) */ - size_t local_subnet_count; /**< Количество локальных подсетей */ + uint32_t *dynamic_subnets; /**< Не используется текущей реализацией; destroy освобождает память */ + size_t dynamic_subnet_count; /**< Не используется текущей реализацией */ + uint32_t *local_subnets; /**< Не используется текущей реализацией; destroy освобождает память */ + size_t local_subnet_count; /**< Не используется текущей реализацией */ struct { uint64_t local_routes; /**< Количество локальных маршрутов */ uint64_t learned_routes; /**< Количество изученных маршрутов */ @@ -80,10 +82,12 @@ struct ROUTE_TABLE *route_table_create(uint64_t my_node_id); void route_table_destroy(struct ROUTE_TABLE *table); /** - * @brief Вставляет в таблицу маршрутизации все подсети для указанного узла + * @brief Атомарно вставляет все IPv4-подсети узла; /0..32, адреса нормализуются по маске + * @param table Таблица без пересечений; перед заменой маршрутов узла вызвать route_delete * @param node узел, все маршруты которого нужно добавить * - * @return true если подсети добавлены; false при отсутствии подсетей, пересечении или ошибке выделения памяти + * @return true если весь набор добавлен; false при неверных аргументах/префиксе, отсутствии подсетей, + * пересечении с таблицей/внутри набора или нехватке памяти. При false таблица не меняется. */ bool route_insert(struct ROUTE_TABLE *table, struct TOPO_GROUP_NODE *node); @@ -112,7 +116,7 @@ struct ROUTE_ENTRY* route_lookup(struct ROUTE_TABLE *table, uint32_t dest_ip); void route_table_print(const struct ROUTE_TABLE *table); /** - * @brief Парсит строку подсети в сетевой адрес и длину префикса + * @brief Разбирает IPv4/префикс; host-биты адреса сохраняются, префикс читается через atoi * * @param subnet_str Строка подсети (например, "192.168.1.0/24") * @param network Указатель для сохранения адреса в host order @@ -121,8 +125,6 @@ void route_table_print(const struct ROUTE_TABLE *table); */ int parse_subnet(const char *subnet_str, uint32_t *network, uint8_t *prefix_length); -bool route_add_local_subnet(struct ROUTE_TABLE *table, uint32_t network, uint8_t prefix_length); - /** * @brief Проверяет что IP адрес НЕ является публичным (роутится в интернете) * diff --git a/src/routing_layer/route_lib.txt b/src/routing_layer/route_lib.txt index a1c0dd76..88fff76f 100644 --- a/src/routing_layer/route_lib.txt +++ b/src/routing_layer/route_lib.txt @@ -1,31 +1,34 @@ -В таблице роутинга маршруты не пересекаются. Т.е. не может быть одновременно 192.168.1.1/24 и 192.168.1.100/30 - -Как работает роутинг: -1. типов маршрута бывает два: learned (роутятся в ETCP instance) и local (роутятся в TUN). - local - локальные маршруты из конфига (опция конфига my_subnet=IP/Mask) - при инициализации local сразу добавляются в роутинг таблицу (ипользуя запись BGP_NODEINFO_PACKET* my_nodeinfo). - -BGP_NODEINFO собирается на узле владельце node, далее распространяется по остальным узлам. - -2. При установке подключения к новому узлу мы отправляем этому узлу все nodeinfo. Если несколько доступных линков - отправляем только один - preferred_conn -3. При изменении preferred_conn рассылаем reroute (например старый preferred_conn удален) -3. При удалении всех подключений рассылаем withdraw. - -3. При получении BGP_NODEINFO_PACKET мы смотрим есть ли такой узел уже в таблице. Если узла нет - добавляем, если есть - обновляем информацию о узле включая его маршруты и список next_hop через которые доступен узел (nb_routes). - Добавляем так: - инкрементируем hop_count - устанавливая etcp линк с которого приняли как next_hop, добавляем его в hop_list. - рассылаем по всем активным линкам кроме линка с которого получили (обязательно) - Обновление: обновляем hop_list если поменялся - -4. При отключении от узла (если у node не осталось nb_routes) мы: - Удаляем node и вложенные структуры: все маршруты узла - Рассылаем withdraw для узла hop_id. - -Логика рассылки withdraw: - Если получен withdraw - удаляем этот маршрут и распространяем withdraw или reroute по всем линкам кроме того с которого получили. В зависимости от изменений (остались ли резервные линки или изменился preferred_conn). - -Метрик маршрута пока нет. используется первый доступный. - -Как работаеут маршрутизация: - destination IP -> route table lookup -> route entry (ip/mask, node_info*) -> выбор лучшего next_hop -> отправка в next_hop +route_lib — локальная таблица IPv4-подсетей UTUN +================================================ + +Подсети в таблице не пересекаются. Запрещены одинаковые и вложенные диапазоны, +как между узлами, так и внутри одного переданного набора. Например, +10.0.0.0/8 исключает 10.1.0.0/24; соседние 10.1.0.0/24 и 10.1.1.0/24 допустимы. +Маршрут /0 занимает всё адресное пространство и может быть только единственным. + +route_insert(table, node) добавляет все node->subnets->v4_subnets атомарно. +Префиксы должны быть в диапазоне 0..32; адреса нормализуются маской перед хранением. +При неверных аргументах, отсутствии подсетей, пересечении или OOM возвращает false, +не меняя таблицу и её статистику. Перед заменой маршрутов узла вызывают route_delete. + +Таблица — отсортированный массив. route_lookup получает IPv4 в host byte order +и бинарным поиском возвращает единственный подходящий диапазон либо NULL. +Это не LPM-таблица: вложенные префиксы исключены при вставке. +Указатель на ROUTE_ENTRY действителен только до следующего изменения таблицы. +Адреса TOPO_SUBNET4 хранятся в сетевых байтах, parse_subnet возвращает host order, +is_local_subnet принимает network order. parse_subnet сохраняет host-биты и читает +префикс через atoi; это не строгий валидатор всей строки CIDR. + +TOPO_GROUP_NODE заимствован, в том числе для локальных маршрутов. +Маршруты удаляют до освобождения узла. destroy освобождает массив, но не узлы. +Все вызовы выполняются в одном потоке ядра. + +Наполнение таблицы и поиск следующего узла — разные задачи: + IPv4 destination -> route_lookup -> TOPO_GROUP_NODE -> etcp_route_send + -> выбор живого next hop через CM/BGP -> ETCP. +Для локального владельца routing доставляет пакет в TUN после firewall. +Групповые пути, WITHDRAW, выбор по числу переходов и READY описаны в topo_group.h; +общая подписанная запись узла — в topo_node.h и doc/node_snapshot.md. + +IPv6-таблица route6_lib использует radix и разрешает вложенные префиксы. +Основной routing.c пока доставляет только IPv4. diff --git a/src/routing_layer/route_ping.h b/src/routing_layer/route_ping.h index 0f21f666..45b0d2ad 100644 --- a/src/routing_layer/route_ping.h +++ b/src/routing_layer/route_ping.h @@ -1,5 +1,6 @@ /* Прямые UDP-наблюдения с указанного рабочего сокета. Управление помощниками — - * nat_detection; этот модуль не вычисляет NAT-тип и не выбирает чужой сокет. */ + * nat_detection; этот модуль не вычисляет NAT-тип и не выбирает чужой сокет. + * Вызовы с ETCP_SOCKET выполняются в потоке его ядра; буферы адресов/тегов предоставляет вызывающий. */ #ifndef ROUTE_PING_H #define ROUTE_PING_H @@ -14,16 +15,25 @@ extern "C" { #define ROUTE_ADDR_WIRE_SIZE 19 #define ROUTE_OBSERVE_TAG_SIZE 33 +/* family(1:4/6), IP(16: IPv4 + 12 нулей либо IPv6), port(2 BE), без IPv6 scope_id. + * encode/decode: 0/-1; decode также отвергает нулевой порт и ненулевой IPv4-padding. + * При ошибке выход может быть частично заполнен. encode сам по себе нулевой порт не запрещает. */ int route_ping_addr_encode(uint8_t out[ROUTE_ADDR_WIRE_SIZE], const struct sockaddr_storage *address); int route_ping_addr_decode(const uint8_t in[ROUTE_ADDR_WIRE_SIZE], struct sockaddr_storage *address); +/* 1 — совпадают семейство/IP и (при include_port!=0) порт; 0 — иначе. IPv6 scope_id не сравнивается. */ int route_ping_addr_equal(const struct sockaddr_storage *a, const struct sockaddr_storage *b, int include_port); -/* Ответ: address(19) || tag(33); tag связывает socket/generation/round/group. Числа BE, IP — сетевые байты. */ +/* Отправить OBSERVE PING: group/round и generation сокета ненулевые, timeout_ms — мс. + * peer_key — 32 байта ключа наблюдателя, peer_address — его UDP endpoint. + * Возвращает 0 при запуске, отрицательный код ошибки ETCP при отказе; callback/arg — контракт ETCP. + * Ответ: address(19) || tag(33); профиль сокета здесь не изменяется. */ int route_ping_observe(struct ETCP_SOCKET *socket, const uint8_t peer_key[SC_PUBKEY_SIZE], const struct sockaddr_storage *peer_address, uint64_t group, uint64_t round, int timeout_ms, etcp_ping_callback_t callback, void *arg); +/* Проверить точные размер/тег и декодировать observed; 0/-1. Подлинность ответа проверяет ETCP. */ int route_ping_observation(const uint8_t *data, size_t length, const struct ETCP_SOCKET *socket, uint64_t group, uint64_t round, struct sockaddr_storage *observed); +/* NAT4UDP!(8) || socket_id(1) || generation(8 BE) || round(8 BE) || group(8 BE); out/socket обязательны. */ void route_ping_make_tag(uint8_t out[ROUTE_OBSERVE_TAG_SIZE], const struct ETCP_SOCKET *socket, uint64_t group, uint64_t round); #ifdef __cplusplus diff --git a/src/routing_layer/route_ping_doc.md b/src/routing_layer/route_ping_doc.md index 9729b733..97c4a663 100644 --- a/src/routing_layer/route_ping_doc.md +++ b/src/routing_layer/route_ping_doc.md @@ -4,15 +4,16 @@ `ETCP_SOCKET`. Выбор B/C, пересылка управляющих сообщений и классификация профиля принадлежат `nat_detection`; алгоритм — в `doc/nat_detection.txt`. -`route_ping_observe(socket, peer_key, peer_address, round, timeout_ms, callback, arg)` +`route_ping_observe(socket, peer_key, peer_address, group, round, timeout_ms, callback, arg)` передаёт PING с `ETCP_PING_FLAG_OBSERVE`. PONG содержит фактически наблюдаемый source IP:port и исходный тег. Профиль не меняется от единственного ответа. -`route_ping_observation` проверяет размер, ID/поколение сокета и round. +`route_ping_observation` проверяет размер, ID/поколение сокета, round и group. Адрес занимает 19 байт: family(1: 4/6), IP(16: IPv4 + 12 нулей либо IPv6), -port(2, BE). Тег занимает 25 байт: `NAT2UDP!`(8), socket_id(1), -generation(8, BE), round(8, BE). Неправильные family, скрытые IPv4-байты -и нулевой порт отвергаются. Новый wire не использует native packed integers. +port(2, BE). Тег занимает 33 байта: `NAT4UDP!`(8), socket_id(1), +generation(8, BE), round(8, BE), group(8, BE). Декодирование отвергает неправильные +family, скрытые IPv4-байты и нулевой порт. IPv6 scope_id в wire не передаётся. +Кодирование нулевой порт не запрещает. Числа тега передаются в BE. В транспорте nonce генерируется CSPRNG; PONG должен соответствовать ожидаемому ключу, source IP:port и принимающему socket generation. Pending снимается @@ -20,8 +21,8 @@ generation(8, BE), round(8, BE). Неправильные family, скрытые совпадающие запросы без callback; arg остаётся собственностью вызывающего. `ETCP_PING_FLAG_ADDRESS_CHECK` обозначает прямое входящее испытание. Его -receipt фиксируется на рабочем сокете владельца до отправки PONG. Потеря -обратного PONG поэтому не превращает полученный ingress в отрицательный результат. +приём передаётся `nat_detection_receive_ingress` без UDP-ответа: ответ мог бы +испортить историю контактов. Результат передаётся по защищённому управляющему каналу. UDP-результаты не используются для TCP listener и SOCKS5 endpoint. Проверки: `tests/test_route_ping.c`, `tests/test_nat_detection.c`, diff --git a/src/routing_layer/route_tx_stats.h b/src/routing_layer/route_tx_stats.h index 157d8657..c1b7db5a 100644 --- a/src/routing_layer/route_tx_stats.h +++ b/src/routing_layer/route_tx_stats.h @@ -1,6 +1,7 @@ /* Учёт первой доставки payload логического канала. Один uasync-поток. * Десять завершённых интервалов по 50мс и текущий интервал; reset начинает новую историю. - * Повторы учитывает вызывающий: add вызывается только при снятии подтверждённого inflight. */ + * Повторы учитывает вызывающий: ack вызывается только при снятии подтверждённого inflight. + * Время — монотонные микросекунды; память всех структур принадлежит вызывающему. */ #ifndef ROUTE_TX_STATS_H #define ROUTE_TX_STATS_H @@ -14,16 +15,21 @@ extern "C" { #define ROUTE_TX_WINDOW_US 500000u #define ROUTE_TX_BUCKETS 11 +/* tick_plus_one=0 означает пустую ячейку; иначе это номер 50-мс интервала + 1. */ struct route_tx_bucket { uint64_t tick_plus_one, bytes, packets; }; struct route_tx_meter { struct route_tx_bucket buckets[ROUTE_TX_BUCKETS]; - uint64_t started_us, last_us, bytes, packets; + uint64_t started_us, last_us, bytes, packets; /* reset/последний ACK и накопленные итоги эпохи */ }; +/* end_us — правая граница окна; valid=1 после накопления полных 500 мс с момента reset. */ struct route_tx_window { uint64_t end_us, bytes, packets; int valid; }; +/* Обнулить историю и начать эпоху в now_us; meter обязателен. */ void route_tx_meter_reset(struct route_tx_meter *meter, uint64_t now_us); +/* Учесть один впервые подтверждённый payload (bytes>0); 0/-1, при ошибке история не меняется. */ int route_tx_meter_ack(struct route_tx_meter *meter, uint64_t now_us, uint64_t bytes); -/* Неполное окно после reset возвращает valid=0; текущая незавершённая ячейка исключается. */ +/* Заполнить out суммой завершённых ячеек; 0/-1 (при ошибке out не меняется). + * Неполное окно после reset возвращает valid=0; текущая незавершённая ячейка исключается. */ int route_tx_meter_window(const struct route_tx_meter *meter, uint64_t now_us, struct route_tx_window *out); #ifdef __cplusplus diff --git a/src/routing_layer/routing.h b/src/routing_layer/routing.h index 49c73adc..82eaf495 100644 --- a/src/routing_layer/routing.h +++ b/src/routing_layer/routing.h @@ -33,7 +33,7 @@ int routing_create(struct UTUN_INSTANCE* instance); int routing_bind(struct UTUN_INSTANCE* instance); /** - * @brief Destroy routing module for instance + * @brief Снять DATA binding и освободить IPv4-таблицу; callback TUN снимается при остановке самого TUN * @param instance UTUN instance */ void routing_destroy(struct UTUN_INSTANCE* instance); @@ -58,7 +58,9 @@ void routing_del_conn(struct ETCP_CONN* etcp); void routing_set_tun(struct UTUN_INSTANCE* instance); /** - * @brief Route a single packet (exposed for NAT module interception) + * @brief Маршрутизировать [cmd:1][IPv4] в TUN либо через TOPO_GROUP_UTUN; требует готовых instance->rt/tun + * @param entry Передаваемые entry и dgram: модуль забирает владение при любом исходе + * @param src_node_id Исходный узел для диагностики, для выхода из TUN — instance->node_id */ void route_pkt(struct UTUN_INSTANCE* instance, struct ll_entry* entry, uint64_t src_node_id); diff --git a/src/routing_layer/routing_doc.md b/src/routing_layer/routing_doc.md index b904a554..d56f11ee 100644 --- a/src/routing_layer/routing_doc.md +++ b/src/routing_layer/routing_doc.md @@ -20,8 +20,13 @@ IPv6-пакеты на данный момент дропаются на эта **Ключевые нюансы:** - `routing_add_conn` / `routing_del_conn` — **deprecated** (no-op), маршруты теперь управляются через BGP и таблицу `route_lib` - Все не-IPv4 пакеты (IPv6, unknown) дропаются в `extract_dst_ip` -- Multicast (224.0.0.0/4) и limited broadcast (255.255.255.255) дропаются без логирования +- Multicast (224.0.0.0/4) и limited broadcast (255.255.255.255) отбрасываются с DEBUG-логом routing - Пакеты без маршрута дропаются с инкрементом `instance->dropped_packets` +- `route_pkt` забирает entry и dgram при любом исходе; ожидает `[cmd:1][IPv4]` и готовые `rt`/`tun`. +- Удалённая доставка идёт через `TOPO_GROUP_UTUN`; локальная — в `tun->input_queue`. + Firewall проверяется перед маршрутизацией выхода из TUN и перед локальной доставкой в TUN. +- Все вызовы выполняются в потоке ядра. `routing_destroy` снимает DATA binding и освобождает таблицу; + callback очереди TUN снимается при остановке самого TUN/UTUN-сервиса. ## 3. API diff --git a/src/routing_layer/sock_match.h b/src/routing_layer/sock_match.h index 1457dacd..7d1ef01e 100644 --- a/src/routing_layer/sock_match.h +++ b/src/routing_layer/sock_match.h @@ -21,17 +21,22 @@ struct ETCP_SOCKET; #define SOCK_TC_PROBE 4 struct sock_view { - struct ETCP_SOCKET *sock; - uint8_t role, has_inet, has_priv, family, is_tcp; - uint32_t local_ip4, public_ip4; - struct address_profile address_profile; + struct ETCP_SOCKET *sock; /* заимствован; не переживает удаление сокета */ + uint8_t role, has_inet, has_priv, family, is_tcp; /* SOCK_ROLE_*, доступ к Internet/LAN, AF_INET, TCP-флаг */ + uint32_t local_ip4, public_ip4; /* network order; public_ip4=0, если публичный адрес неизвестен */ + struct address_profile address_profile; /* копия текущего профиля */ }; -/* public_address — классификация IP в соответствующем семействе, не доказательство входа. */ +/* Возвращает SOCK_TC_*; address_type=TOPO_ADDR_*, config_type=CFG_SERVER_TYPE_*. + * public_address — классификация IP в соответствующем семействе, не доказательство входа. */ uint8_t sock_target_class(uint8_t address_type, uint8_t config_type, const struct address_profile *profile, int public_address); +/* 1 — разрешена попытка по target_ip4 (network order), 0 — нет; проверяет has_priv/has_inet. + * Семейство/протокол и выбор маршрута проверяет вызывающий; profile может быть NULL. */ int sock_match(const struct sock_view *view, uint8_t address_type, uint8_t config_type, const struct address_profile *profile, uint32_t target_ip4); +/* В потоке ядра заполнить до max views обычных IPv4-сокетов, включая TCP; probe_only исключены. + * Возвращает число заполненных (0 также при неверных аргументах); специальной сортировки нет. */ int sock_collect_views(struct UTUN_INSTANCE *instance, struct sock_view *out, int max); #endif diff --git a/src/routing_layer/topo_group.c b/src/routing_layer/topo_group.c index a3d93665..f9a679b2 100644 --- a/src/routing_layer/topo_group.c +++ b/src/routing_layer/topo_group.c @@ -1265,7 +1265,12 @@ int topo_group_process_nodeinfo(struct TOPO_GROUP* group, struct ETCP_CONN* from } int dyn = topo_node_dyn_size(ni); - if ((size_t)dyn + sizeof(struct TOPOMSG_NODEINFO_PKT) != len) { DEBUG_WARN(DEBUG_CATEGORY_BGP, "Incorrect packet size (%s)", from->log_name); return -1; } + if (dyn < 0) return -1; // Причина уже записана в topo_node_dyn_size. + if ((size_t)dyn != len - sizeof(struct TOPOMSG_NODEINFO_PKT)) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO size mismatch from=%s node=%016llx length=%zu expected=%zu", + from->log_name, (unsigned long long)ni->node_id, len, sizeof(struct TOPOMSG_NODEINFO_PKT) + (size_t)dyn); + return -1; + } uint64_t node_id = ni->node_id; if (ni->hop_count >= MAX_HOPS) { DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO from %s dropped: too many hops (%d)", from->log_name, ni->hop_count); return -1; } diff --git a/src/routing_layer/topo_node.c b/src/routing_layer/topo_node.c index f0233dc4..54bf1712 100644 --- a/src/routing_layer/topo_node.c +++ b/src/routing_layer/topo_node.c @@ -1,6 +1,7 @@ #include #include #include +#include #include "../lib/ll_queue.h" #include "../lib/debug_config.h" @@ -475,15 +476,20 @@ int topo_node_verify(const struct TOPO_NODE* ni) { /* Вычисляет суммарный размер динамической части NODEINFO по счётчикам заголовка. */ int topo_node_dyn_size(const struct TOPOMSG_NODE* msg) { + if (!msg) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "NODEINFO size: missing header"); return -1; } + if (!(msg->flags & TOPO_FLAG_SEND_SUBNETS) && (msg->local_v4_subnets || msg->local_v6_subnets)) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO size: subnets without flag node=%016llx flags=%02x v4=%u v6=%u", + (unsigned long long)msg->node_id, msg->flags, msg->local_v4_subnets, msg->local_v6_subnets); + return -1; + } return msg->node_name_len + msg->local_v4_sockets * sizeof(struct TOPOMSG_SOCKMETA4) + msg->local_v4_addrs * sizeof(struct TOPOMSG_ADDR4) + msg->local_v6_sockets * sizeof(struct TOPOMSG_SOCKMETA6) + msg->local_v6_addrs * sizeof(struct TOPOMSG_ADDR6) + msg->local_reality_socks * sizeof(struct TOPOMSG_REALITY_SOCK) - + ((msg->flags & TOPO_FLAG_SEND_SUBNETS) - ? (msg->local_v4_subnets * sizeof(struct TOPOMSG_SUBNET4) + msg->local_v6_subnets * sizeof(struct TOPOMSG_SUBNET6)) - : 0) + + msg->local_v4_subnets * sizeof(struct TOPOMSG_SUBNET4) + + msg->local_v6_subnets * sizeof(struct TOPOMSG_SUBNET6) + msg->hop_count * 8; } @@ -491,7 +497,7 @@ int topo_node_dyn_size(const struct TOPOMSG_NODE* msg) { int topo_node_serialize(struct TOPO_NODE* ni, struct TOPO_GROUP_NODE* nq, uint64_t group_id, uint8_t flags, uint8_t* out, size_t out_max, uint16_t cumulative_rtt) { - if (!ni || !nq || !out) return -1; + if (!ni || !nq || !out) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "NODEINFO encode: missing node/group/output"); return -1; } struct TOPOMSG_NODE msg; memset(&msg, 0, sizeof(msg)); @@ -510,7 +516,8 @@ int topo_node_serialize(struct TOPO_NODE* ni, struct TOPO_GROUP_NODE* nq, topo_list_count((struct _topo_head*)ni->v6_sock_meta) > UINT8_MAX || topo_list_count((struct _topo_head*)ni->v6_addrs) > UINT8_MAX || topo_list_count((struct _topo_head*)ni->reality_socks) > UINT8_MAX || - (nq->subnets && (topo_list_count((struct _topo_head*)nq->subnets->v4_subnets) > UINT8_MAX || + ((flags & TOPO_FLAG_SEND_SUBNETS) && nq->subnets && + (topo_list_count((struct _topo_head*)nq->subnets->v4_subnets) > UINT8_MAX || topo_list_count((struct _topo_head*)nq->subnets->v6_subnets) > UINT8_MAX))) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node exceeds wire format limits"); return -1; } @@ -519,18 +526,25 @@ int topo_node_serialize(struct TOPO_NODE* ni, struct TOPO_GROUP_NODE* nq, msg.local_v6_sockets = topo_list_count((struct _topo_head*)ni->v6_sock_meta); msg.local_v6_addrs = topo_list_count((struct _topo_head*)ni->v6_addrs); msg.local_reality_socks = topo_list_count((struct _topo_head*)ni->reality_socks); - msg.local_v4_subnets = nq->subnets ? topo_list_count((struct _topo_head*)nq->subnets->v4_subnets) : 0; - msg.local_v6_subnets = nq->subnets ? topo_list_count((struct _topo_head*)nq->subnets->v6_subnets) : 0; - { uint8_t hc; uint64_t* hl = topo_node_best_hop_list(nq, &hc, NULL); - msg.hop_count = hc; } + if ((flags & TOPO_FLAG_SEND_SUBNETS) && nq->subnets) { + msg.local_v4_subnets = topo_list_count((struct _topo_head*)nq->subnets->v4_subnets); + msg.local_v6_subnets = topo_list_count((struct _topo_head*)nq->subnets->v6_subnets); + } + uint64_t *hop_list = topo_node_best_hop_list(nq, &msg.hop_count, NULL); msg.client_type = ni->client_type; msg.client_activity = ni->client_activity; msg.cumulative_rtt = cumulative_rtt; if (nq->radio) msg.flags |= TOPO_FLAG_RADIO; msg.flags |= nq->phone_status; - size_t total = TOPOMSG_NODE_HDR_SIZE + (size_t)topo_node_dyn_size(&msg); - if (total > out_max) return -1; + int dyn = topo_node_dyn_size(&msg); + if (dyn < 0) return -1; // Причина уже записана в topo_node_dyn_size. + size_t total = TOPOMSG_NODE_HDR_SIZE + (size_t)dyn; + if (total > out_max) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO encode: output too small node=%016llx need=%zu capacity=%zu", + (unsigned long long)ni->node_id, total, out_max); + return -1; + } memcpy(out, &msg, TOPOMSG_NODE_HDR_SIZE); uint8_t* dp = out + TOPOMSG_NODE_HDR_SIZE; @@ -574,8 +588,9 @@ int topo_node_serialize(struct TOPO_NODE* ni, struct TOPO_GROUP_NODE* nq, }} } } - { uint8_t hc; uint64_t* hl = topo_node_best_hop_list(nq, &hc, NULL); - if (hc && hl) { size_t sz = hc * 8; memcpy(dp, hl, sz); dp += sz; } } + if (msg.hop_count) { size_t size = msg.hop_count * sizeof(*hop_list); memcpy(dp, hop_list, size); dp += size; } + DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "NODEINFO encoded: node=%016llx bytes=%zu flags=%02x subnets=%u/%u hops=%u", + (unsigned long long)ni->node_id, total, msg.flags, msg.local_v4_subnets, msg.local_v6_subnets, msg.hop_count); return (int)(dp - out); } @@ -585,129 +600,190 @@ int topo_node_serialize(struct TOPO_NODE* ni, struct TOPO_GROUP_NODE* nq, int topo_node_deserialize(struct TOPO_GROUP* group, const uint8_t* data, size_t len, struct TOPO_NODE** out_ni, struct TOPO_NODESUBNETS** out_subnets, uint64_t** out_hop_list, uint8_t* out_hop_count, uint16_t* out_cumulative_rtt) { - if (!group || !data || len < TOPOMSG_NODE_HDR_SIZE || !out_ni) return -1; - + if (out_ni) *out_ni = NULL; + if (out_subnets) *out_subnets = NULL; + if (out_hop_list) *out_hop_list = NULL; + if (out_hop_count) *out_hop_count = 0; + if (out_cumulative_rtt) *out_cumulative_rtt = 0; + if (!group || !group->instance || !group->instance->topo_groups || !data || !out_ni) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "NODEINFO decode: missing group/instance/registry/data/output"); + return -1; + } + DEBUG_TRACE(DEBUG_CATEGORY_BGP, "NODEINFO decode: length=%zu", len); + if (len < TOPOMSG_NODE_HDR_SIZE) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO decode: short header length=%zu need=%zu", len, TOPOMSG_NODE_HDR_SIZE); + return -1; + } const struct TOPOMSG_NODE* msg = (const struct TOPOMSG_NODE*)data; int dyn = topo_node_dyn_size(msg); - if ((size_t)dyn + TOPOMSG_NODE_HDR_SIZE != len) return -1; + if (dyn < 0) return -1; // Причина уже записана в topo_node_dyn_size. + if ((size_t)dyn != len - TOPOMSG_NODE_HDR_SIZE) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO decode: size mismatch node=%016llx length=%zu expected=%zu flags=%02x subnets=%u/%u hops=%u", + (unsigned long long)msg->node_id, len, TOPOMSG_NODE_HDR_SIZE + (size_t)dyn, + msg->flags, msg->local_v4_subnets, msg->local_v6_subnets, msg->hop_count); + return -1; + } - struct TOPO_NODE* ni = u_calloc(1, sizeof(struct TOPO_NODE)); - if (!ni) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "alloc TOPO_NODE failed"); return -1; } - ni->group_ref_count = 0; - ni->node_id = msg->node_id; - ni->ver = msg->ver; - ni->timestamp = msg->timestamp; + struct TOPO_GROUPS* groups = group->instance->topo_groups; + struct TOPO_NODE* ni = u_calloc(1, sizeof(*ni)); + struct TOPO_NODESUBNETS* subnets = NULL; + uint64_t* hops = NULL; + const uint8_t* dp = data + TOPOMSG_NODE_HDR_SIZE; + const char* section = "node"; + unsigned index = 0; + if (!ni) goto no_memory; + ni->node_id = msg->node_id; ni->ver = msg->ver; ni->timestamp = msg->timestamp; memcpy(ni->public_key, msg->public_key, SC_PUBKEY_SIZE); memcpy(ni->ed25519_public_key, msg->ed25519_public_key, SC_PUBKEY_SIZE); memcpy(ni->x25519_self_sig, msg->x25519_self_sig, 64); - ni->client_type = msg->client_type; - ni->client_activity = msg->client_activity; - - const uint8_t* dp = data + TOPOMSG_NODE_HDR_SIZE; + ni->client_type = msg->client_type; ni->client_activity = msg->client_activity; + section = "name"; if (msg->node_name_len) { ni->node_name = u_malloc(msg->node_name_len + 1); - if (ni->node_name) { memcpy(ni->node_name, dp, msg->node_name_len); ni->node_name[msg->node_name_len] = 0; } - dp += msg->node_name_len; + if (!ni->node_name) goto no_memory; + memcpy(ni->node_name, dp, msg->node_name_len); ni->node_name[msg->node_name_len] = 0; } + dp += msg->node_name_len; - { struct TOPO_SOCKMETA4* head = NULL; struct TOPO_SOCKMETA4* tail = NULL; - for (int i = 0; i < msg->local_v4_sockets; i++) { + /* Списки сразу принадлежат ni: общий выход ошибки видит и незавершённую секцию. */ + section = "v4 socket metadata"; + { struct TOPO_SOCKMETA4** tail = &ni->v4_sock_meta; + for (index = 0; index < msg->local_v4_sockets; index++) { const struct TOPOMSG_SOCKMETA4* w = (const struct TOPOMSG_SOCKMETA4*)dp; dp += sizeof(*w); - struct TOPO_SOCKMETA4* e = memory_pool_alloc(group->instance->topo_groups->v4_sock_meta_pool); - if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; } - e->id = w->id; e->config_type = w->config_type; e->next = NULL; - if (sockmeta_wire_read((const uint8_t *)w, &e->address_profile)) { - memory_pool_free(group->instance->topo_groups->v4_sock_meta_pool, e); - ni->v4_sock_meta = head; topo_node_free_raw(group->instance->topo_groups, ni); return -1; - } - if (!head) head = tail = e; else { tail->next = e; tail = e; } + struct TOPO_SOCKMETA4* e = memory_pool_alloc(groups->v4_sock_meta_pool); + if (!e) goto no_memory; + *tail = e; tail = &e->next; + e->id = w->id; e->config_type = w->config_type; + if (sockmeta_wire_read((const uint8_t*)w, &e->address_profile)) goto invalid_profile; } - ni->v4_sock_meta = head; } - { struct TOPO_ADDR4* head = NULL; struct TOPO_ADDR4* tail = NULL; - for (int i = 0; i < msg->local_v4_addrs; i++) { + } + section = "v4 addresses"; + { struct TOPO_ADDR4** tail = &ni->v4_addrs; + for (index = 0; index < msg->local_v4_addrs; index++) { const struct TOPOMSG_ADDR4* w = (const struct TOPOMSG_ADDR4*)dp; dp += sizeof(*w); - struct TOPO_ADDR4* e = memory_pool_alloc(group->instance->topo_groups->v4_addr_pool); - if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; } - memcpy(e->addr, w->addr, 4); e->port = w->port; e->type = w->type; e->socket_id = w->socket_id; e->protocol = w->protocol; e->next = NULL; - if (!head) head = tail = e; else { tail->next = e; tail = e; } + struct TOPO_ADDR4* e = memory_pool_alloc(groups->v4_addr_pool); + if (!e) goto no_memory; + *tail = e; tail = &e->next; + memcpy(e->addr, w->addr, 4); e->port = w->port; e->type = w->type; + e->socket_id = w->socket_id; e->protocol = w->protocol; } - ni->v4_addrs = head; } - { struct TOPO_SOCKMETA6* head = NULL; struct TOPO_SOCKMETA6* tail = NULL; - for (int i = 0; i < msg->local_v6_sockets; i++) { + } + section = "v6 socket metadata"; + { struct TOPO_SOCKMETA6** tail = &ni->v6_sock_meta; + for (index = 0; index < msg->local_v6_sockets; index++) { const struct TOPOMSG_SOCKMETA6* w = (const struct TOPOMSG_SOCKMETA6*)dp; dp += sizeof(*w); - struct TOPO_SOCKMETA6* e = memory_pool_alloc(group->instance->topo_groups->v6_sock_meta_pool); - if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; } - e->id = w->id; e->config_type = w->config_type; e->next = NULL; - if (sockmeta_wire_read((const uint8_t *)w, &e->address_profile)) { - memory_pool_free(group->instance->topo_groups->v6_sock_meta_pool, e); - ni->v6_sock_meta = head; topo_node_free_raw(group->instance->topo_groups, ni); return -1; - } - if (!head) head = tail = e; else { tail->next = e; tail = e; } + struct TOPO_SOCKMETA6* e = memory_pool_alloc(groups->v6_sock_meta_pool); + if (!e) goto no_memory; + *tail = e; tail = &e->next; + e->id = w->id; e->config_type = w->config_type; + if (sockmeta_wire_read((const uint8_t*)w, &e->address_profile)) goto invalid_profile; } - ni->v6_sock_meta = head; } - { struct TOPO_ADDR6* head = NULL; struct TOPO_ADDR6* tail = NULL; - for (int i = 0; i < msg->local_v6_addrs; i++) { + } + section = "v6 addresses"; + { struct TOPO_ADDR6** tail = &ni->v6_addrs; + for (index = 0; index < msg->local_v6_addrs; index++) { const struct TOPOMSG_ADDR6* w = (const struct TOPOMSG_ADDR6*)dp; dp += sizeof(*w); - struct TOPO_ADDR6* e = memory_pool_alloc(group->instance->topo_groups->v6_addr_pool); - if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; } - memcpy(e->addr, w->addr, 16); e->port = w->port; e->type = w->type; e->socket_id = w->socket_id; e->protocol = w->protocol; e->next = NULL; - if (!head) head = tail = e; else { tail->next = e; tail = e; } + struct TOPO_ADDR6* e = memory_pool_alloc(groups->v6_addr_pool); + if (!e) goto no_memory; + *tail = e; tail = &e->next; + memcpy(e->addr, w->addr, 16); e->port = w->port; e->type = w->type; + e->socket_id = w->socket_id; e->protocol = w->protocol; } - ni->v6_addrs = head; } - { struct TOPO_REALITY_SOCK* head = NULL; struct TOPO_REALITY_SOCK* tail = NULL; - for (int i = 0; i < msg->local_reality_socks; i++) { + } + section = "REALITY sockets"; + { struct TOPO_REALITY_SOCK** tail = &ni->reality_socks; + for (index = 0; index < msg->local_reality_socks; index++) { const struct TOPOMSG_REALITY_SOCK* w = (const struct TOPOMSG_REALITY_SOCK*)dp; dp += sizeof(*w); - struct TOPO_REALITY_SOCK* e = u_calloc(1, sizeof(struct TOPO_REALITY_SOCK)); - if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; } + struct TOPO_REALITY_SOCK* e = u_calloc(1, sizeof(*e)); + if (!e) goto no_memory; + *tail = e; tail = &e->next; e->socket_id = w->socket_id; - memcpy(e->short_id, w->short_id, 8); - memcpy(e->server_pubkey, w->server_pubkey, 32); - memcpy(e->version, w->version, 3); - memcpy(e->server_name, w->server_name, TOPO_REALITY_SNAME_MAX); + memcpy(e->short_id, w->short_id, 8); memcpy(e->server_pubkey, w->server_pubkey, 32); + memcpy(e->version, w->version, 3); memcpy(e->server_name, w->server_name, TOPO_REALITY_SNAME_MAX); e->server_name[TOPO_REALITY_SNAME_MAX - 1] = 0; - e->next = NULL; - if (!head) head = tail = e; else { tail->next = e; tail = e; } } - ni->reality_socks = head; } + } - if (out_subnets && (msg->flags & TOPO_FLAG_SEND_SUBNETS) && (msg->local_v4_subnets || msg->local_v6_subnets)) { - struct TOPO_NODESUBNETS* r = u_calloc(1, sizeof(struct TOPO_NODESUBNETS)); - if (!r) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; } - for (int i = 0; i < msg->local_v4_subnets; i++) { + section = "subnets"; index = 0; + if (out_subnets && (msg->local_v4_subnets || msg->local_v6_subnets)) { + subnets = u_calloc(1, sizeof(*subnets)); + if (!subnets) goto no_memory; + } + /* Курсор проходит все присутствующие секции; out_subnets управляет только выделением памяти. */ + section = "v4 subnets"; + { struct TOPO_SUBNET4** tail = subnets ? &subnets->v4_subnets : NULL; + for (index = 0; index < msg->local_v4_subnets; index++) { const struct TOPOMSG_SUBNET4* w = (const struct TOPOMSG_SUBNET4*)dp; dp += sizeof(*w); - struct TOPO_SUBNET4* e = memory_pool_alloc(group->instance->topo_groups->v4_subnet_pool); - if (!e) { u_free(r); topo_node_free_raw(group->instance->topo_groups, ni); return -1; } + if (w->prefix_length > 32) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO decode: invalid IPv4 prefix node=%016llx index=%u prefix=%u", + (unsigned long long)msg->node_id, index, w->prefix_length); + goto fail; + } + if (!subnets) continue; + struct TOPO_SUBNET4* e = memory_pool_alloc(groups->v4_subnet_pool); + if (!e) goto no_memory; + *tail = e; tail = &e->next; memcpy(e->addr, w->addr, 4); e->prefix_length = w->prefix_length; - e->next = r->v4_subnets; r->v4_subnets = e; } - for (int i = 0; i < msg->local_v6_subnets; i++) { + } + section = "v6 subnets"; + { struct TOPO_SUBNET6** tail = subnets ? &subnets->v6_subnets : NULL; + for (index = 0; index < msg->local_v6_subnets; index++) { const struct TOPOMSG_SUBNET6* w = (const struct TOPOMSG_SUBNET6*)dp; dp += sizeof(*w); - struct TOPO_SUBNET6* e = memory_pool_alloc(group->instance->topo_groups->v6_subnet_pool); - if (!e) { u_free(r); topo_node_free_raw(group->instance->topo_groups, ni); return -1; } + if (w->prefix_length > 128) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO decode: invalid IPv6 prefix node=%016llx index=%u prefix=%u", + (unsigned long long)msg->node_id, index, w->prefix_length); + goto fail; + } + if (!subnets) continue; + struct TOPO_SUBNET6* e = memory_pool_alloc(groups->v6_subnet_pool); + if (!e) goto no_memory; + *tail = e; tail = &e->next; memcpy(e->addr, w->addr, 16); e->prefix_length = w->prefix_length; - e->next = r->v6_subnets; r->v6_subnets = e; } - *out_subnets = r; - } else if (!(msg->flags & TOPO_FLAG_SEND_SUBNETS)) { - dp += msg->local_v4_subnets * sizeof(struct TOPOMSG_SUBNET4) - + msg->local_v6_subnets * sizeof(struct TOPOMSG_SUBNET6); - if (out_subnets) *out_subnets = NULL; - } else { - if (out_subnets) *out_subnets = NULL; } - if (out_hop_list && msg->hop_count > 0) { - size_t hsz = msg->hop_count * 8; - *out_hop_list = u_malloc(hsz); - if (!*out_hop_list) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; } - memcpy(*out_hop_list, dp, hsz); dp += hsz; - *out_hop_count = msg->hop_count; + section = "hops"; index = 0; + size_t hop_bytes = msg->hop_count * sizeof(*hops); + if (out_hop_list && hop_bytes) { + hops = u_malloc(hop_bytes); + if (!hops) goto no_memory; + memcpy(hops, dp, hop_bytes); + } + dp += hop_bytes; + if ((size_t)(dp - data) != len) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "NODEINFO decode: cursor mismatch node=%016llx consumed=%zu length=%zu", + (unsigned long long)msg->node_id, (size_t)(dp - data), len); + goto fail; } - if (out_cumulative_rtt) *out_cumulative_rtt = msg->cumulative_rtt; *out_ni = ni; + if (out_subnets) *out_subnets = subnets; + if (out_hop_list) *out_hop_list = hops; + if (out_hop_count) *out_hop_count = msg->hop_count; + if (out_cumulative_rtt) *out_cumulative_rtt = msg->cumulative_rtt; + DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "NODEINFO decoded: node=%016llx bytes=%zu flags=%02x subnets=%u/%u hops=%u keep_subnets=%d keep_hops=%d", + (unsigned long long)msg->node_id, len, msg->flags, msg->local_v4_subnets, msg->local_v6_subnets, + msg->hop_count, out_subnets != NULL, out_hop_list != NULL); return 0; + +invalid_profile: + DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO decode: invalid profile node=%016llx section=%s index=%u", + (unsigned long long)msg->node_id, section, index); + goto fail; +no_memory: + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "NODEINFO decode: allocation failed node=%016llx section=%s index=%u offset=%zu", + (unsigned long long)msg->node_id, section, index, (size_t)(dp - data)); +fail: + topo_node_free_raw(groups, ni); + if (subnets) { + free_v4_sub_list(groups->v4_subnet_pool, subnets->v4_subnets); + free_v6_sub_list(groups->v6_subnet_pool, subnets->v6_subnets); + u_free(subnets); + } + u_free(hops); + return -1; } /* Ищет per-group узел TOPO_GROUP_NODE в группе по node_id (NULL, если нет). */ @@ -896,23 +972,36 @@ void topo_node_dump_all(struct TOPO_GROUP* group) { /* То же, что dump_all, но в строковый буфер (для control-сервера). */ int topo_node_format_all(struct TOPO_GROUP* group, char* buf, size_t buf_size) { - if (!group || !group->nodes || !buf || buf_size == 0) return 0; - int pos = 0; + if (buf && buf_size) buf[0] = '\0'; + if (!group || !group->nodes || !buf || buf_size == 0) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node dump: invalid arguments group=%p buf=%p capacity=%zu", (void*)group, (void*)buf, buf_size); + return 0; + } + if (buf_size > INT_MAX) buf_size = INT_MAX; + size_t pos = 0; - pos += snprintf(buf + pos, buf_size > (size_t)pos ? buf_size - pos : 0, - "=== BGP Nodes dump: %d nodes ===\n", queue_entry_count(group->nodes)); +#define FMT_ADD(...) do { \ + int added = snprintf(buf + pos, buf_size - pos, __VA_ARGS__); \ + if (added < 0) { \ + buf[pos] = '\0'; \ + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node dump: formatting failed after %zu bytes", pos); \ + return -1; \ + } \ + if ((size_t)added >= buf_size - pos) { pos = buf_size - 1; goto truncated; } \ + pos += (size_t)added; \ +} while (0) + + FMT_ADD("=== BGP Nodes dump: %d nodes ===\n", queue_entry_count(group->nodes)); int idx = 0; struct ll_entry* e = group->nodes->head; - while (e && (size_t)pos < buf_size) { + while (e) { struct TOPO_GROUP_NODE* nq = (struct TOPO_GROUP_NODE*)e; struct TOPO_NODE* ni = topo_node_registry_find(group->instance->topo_groups, nq->node_id); if (!ni) { e = e->next; continue; } int is_local = (group->local_node == nq); const char* name_s = ni->node_name ? ni->node_name : ""; -#define FMT_ADD(fmt, ...) pos += snprintf(buf + pos, buf_size > (size_t)pos ? buf_size - pos : 0, fmt, ##__VA_ARGS__) - FMT_ADD("--- Node #%d: id=%016llx name=\"%s\" ver=%u grp=%llu%s ---\n", idx, (unsigned long long)ni->node_id, name_s, ni->ver, (unsigned long long)group->group_id, is_local ? " [LOCAL]" : ""); @@ -977,15 +1066,24 @@ int topo_node_format_all(struct TOPO_GROUP* group, char* buf, size_t buf_size) { FMT_ADD("]\n"); } -#undef FMT_ADD e = e->next; idx++; } +#undef FMT_ADD - if ((size_t)pos >= buf_size) { - pos = (int)buf_size - 14; if (pos < 0) pos = 0; - pos += snprintf(buf + pos, buf_size - pos, "\n[TRUNCATED]\n"); + DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "node dump: group=%016llx nodes=%d written=%zu capacity=%zu", + (unsigned long long)group->group_id, idx, pos, buf_size); + return (int)pos; + +truncated: { + const char marker[] = "\n[TRUNCATED]\n"; + size_t marker_len = sizeof(marker) - 1; + if (marker_len > pos) marker_len = pos; + memcpy(buf + pos - marker_len, marker, marker_len); + buf[pos] = '\0'; + DEBUG_WARN(DEBUG_CATEGORY_BGP, "node dump truncated: group=%016llx written=%zu capacity=%zu", + (unsigned long long)group->group_id, pos, buf_size); + return (int)pos; } - return pos; } // ===== update my nodeinfo ===== @@ -1009,114 +1107,124 @@ int topo_node_init_self(struct UTUN_INSTANCE* instance) { return 0; } -/* Пересоздаёт local_node группы при изменении имени/подсетей/ключей, инкрементируя версию. */ +/* Готовит local_node целиком; отказ подготовки/публикации сохраняет прежний узел и маршруты. */ int topo_group_update_my_nodeinfo(struct UTUN_INSTANCE* instance, struct TOPO_GROUP* group) { - if (!instance || !group) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid args"); return -1; } - size_t name_len = 0; - if (instance->name[0]) { name_len = strlen(instance->name); if (name_len > 63) name_len = 63; } - int vc = 0, vc6 = 0; - struct CFG_ROUTE_ENTRY* s = instance->config->my_subnets; - while (s) { if (s->ip.family == AF_INET) vc++; else if (s->ip.family == AF_INET6) vc6++; s = s->next; } - - // Для chat групп подсети не передаём - if (group->group_type == TOPO_GROUP_TYPE_CHAT) { vc = 0; vc6 = 0; } - - int changed = 1; uint8_t old_ver = 0; - if (group->local_node) { - struct TOPO_NODE* oni = topo_node_registry_find(instance->topo_groups, group->local_node->node_id); - if (oni) { - old_ver = oni->ver; - changed = (vc != (group->local_node->subnets ? topo_list_count((struct _topo_head*)group->local_node->subnets->v4_subnets) : 0)) - || (name_len != (oni->node_name ? strlen(oni->node_name) : 0)) - || (name_len && strncmp(instance->name, oni->node_name ? oni->node_name : "", name_len)) - || (memcmp(oni->public_key, instance->my_keys.public_key, SC_PUBKEY_SIZE) != 0) - || (vc6 != (group->local_node->subnets ? topo_list_count((struct _topo_head*)group->local_node->subnets->v6_subnets) : 0)) - || (oni->client_type != instance->client_type) - || (oni->client_activity != instance->client_activity) - || (group->local_node->radio != group->radio_active) - || (group->local_node->phone_status != call_phone_status(instance)) - || (memcmp(oni->ed25519_public_key, group->ed25519_public_key, SC_PUBKEY_SIZE) != 0); - } + if (!instance || !group || group->instance != instance || !instance->config || !instance->topo_groups) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: invalid instance/group/config"); return -1; } - if (!changed && group->local_node) - DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "my nodeinfo unchanged (identity), ver=%d grp=%016llx", - group->local_node->last_ver, (unsigned long long)group->group_id); - - if (changed) { - struct TOPO_NODE* ni = topo_node_registry_find(instance->topo_groups, instance->node_id); - if (ni) topo_node_ref(ni); /* новая группа удерживает запись до освобождения старой */ - if (group->local_node) { - if (instance->rt) route_delete(instance->rt, group->local_node); - topo_nodeq_free_group_fields(instance->topo_groups, group->local_node); - u_free(group->local_node); - group->local_node = NULL; - } - - group->local_node = u_calloc(1, sizeof(struct TOPO_GROUP_NODE)); - if (!group->local_node) { - if (ni) topo_node_registry_unref(instance->topo_groups, instance->node_id); - DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local group node allocation failed"); return -1; - } - struct TOPO_GROUP_NODE* lq = group->local_node; - - int new_identity = ni == NULL; - if (!ni) ni = u_calloc(1, sizeof(struct TOPO_NODE)); - if (!ni) { u_free(lq); group->local_node = NULL; return -1; } - struct TOPO_NODE old_identity = *ni; - ni->node_id = instance->node_id; - ni->ver = (old_ver % 255) + 1; - ni->client_type = instance->client_type; - ni->client_activity = instance->client_activity; - lq->radio = group->radio_active; - lq->phone_status = call_phone_status(instance); - memcpy(ni->public_key, instance->my_keys.public_key, SC_PUBKEY_SIZE); - memcpy(ni->ed25519_public_key, group->ed25519_public_key, SC_PUBKEY_SIZE); - ni->node_name = u_strdup(instance->name); - if (!ni->node_name || topo_node_sign_self(instance, ni) < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_BGP, "cannot publish local identity"); - u_free(ni->node_name); *ni = old_identity; - if (new_identity) topo_node_free_raw(instance->topo_groups, ni); - else topo_node_registry_unref(instance->topo_groups, instance->node_id); - u_free(lq); group->local_node = NULL; return -1; - } - u_free(old_identity.node_name); - if (new_identity) ni = topo_node_registry_store(instance->topo_groups, ni); - if (!ni) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "cannot register local identity"); return -1; } - lq->node_id = ni->node_id; - lq->last_ver = ni->ver; - lq->last_timestamp = ni->timestamp; - - if (group->group_type != TOPO_GROUP_TYPE_CHAT && (vc || vc6)) { - struct TOPO_NODESUBNETS* r = u_calloc(1, sizeof(struct TOPO_NODESUBNETS)); - s = instance->config->my_subnets; - while (s) { - if (s->ip.family == AF_INET) { - struct TOPO_SUBNET4* sub = memory_pool_alloc(group->instance->topo_groups->v4_subnet_pool); - memcpy(sub->addr, &s->ip.addr.v4, 4); sub->prefix_length = s->netmask; - sub->next = r->v4_subnets; r->v4_subnets = sub; - } else if (s->ip.family == AF_INET6) { - struct TOPO_SUBNET6* sub6 = memory_pool_alloc(group->instance->topo_groups->v6_subnet_pool); - memcpy(sub6->addr, &s->ip.addr.v6, 16); sub6->prefix_length = s->netmask; - sub6->next = r->v6_subnets; r->v6_subnets = sub6; - } - s = s->next; + struct TOPO_GROUPS* groups = instance->topo_groups; + struct TOPO_GROUP_NODE* old = group->local_node; + struct TOPO_GROUP_NODE* lq = u_calloc(1, sizeof(*lq)); + if (!lq) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: allocation failed"); return -1; } + struct TOPO_NODE* ni = NULL; + char* name = NULL; + int new_identity = 0, retained = 0, routes_changed = 0, changed = 1, vc = 0, vc6 = 0; + lq->node_id = instance->node_id; lq->radio = group->radio_active; lq->phone_status = call_phone_status(instance); + if (group->group_type != TOPO_GROUP_TYPE_CHAT) { + struct TOPO_SUBNET4** tail4 = NULL; struct TOPO_SUBNET6** tail6 = NULL; + for (struct CFG_ROUTE_ENTRY* s = instance->config->my_subnets; s; s = s->next) { + if ((s->ip.family != AF_INET && s->ip.family != AF_INET6) || + s->netmask > (s->ip.family == AF_INET ? 32 : 128)) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: invalid subnet family=%d prefix=%u", s->ip.family, s->netmask); + goto fail; + } + if (!lq->subnets) { + lq->subnets = u_calloc(1, sizeof(*lq->subnets)); + if (!lq->subnets) goto allocation_failed; + tail4 = &lq->subnets->v4_subnets; tail6 = &lq->subnets->v6_subnets; + } + if (s->ip.family == AF_INET) { + struct TOPO_SUBNET4* sub = memory_pool_alloc(groups->v4_subnet_pool); + if (!sub) goto allocation_failed; + *sub = (struct TOPO_SUBNET4){.prefix_length = s->netmask}; memcpy(sub->addr, &s->ip.addr.v4, 4); + *tail4 = sub; tail4 = &sub->next; vc++; + } else { + struct TOPO_SUBNET6* sub = memory_pool_alloc(groups->v6_subnet_pool); + if (!sub) goto allocation_failed; + *sub = (struct TOPO_SUBNET6){.prefix_length = s->netmask}; memcpy(sub->addr, &s->ip.addr.v6, 16); + *tail6 = sub; tail6 = &sub->next; vc6++; } - lq->subnets = r; } - - if (group->group_type == TOPO_GROUP_TYPE_CHAT) - DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "my_nodeinfo updated: v4sub=%d v6sub=%d ver=%d grp=%016llx", - vc, vc6, ni->ver, (unsigned long long)group->group_id); - else - DEBUG_INFO(DEBUG_CATEGORY_BGP, "my_nodeinfo updated: v4sub=%d v6sub=%d ver=%d grp=%016llx", - vc, vc6, ni->ver, (unsigned long long)group->group_id); - if (group->group_type != TOPO_GROUP_TYPE_CHAT && instance->rt && !route_insert(instance->rt, lq)) - DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "failed to insert local routes"); - } else { - struct TOPO_NODE* lni = topo_node_registry_find(instance->topo_groups, group->local_node->node_id); - if (lni) group->local_node->last_ver = lni->ver; + if (vc > UINT8_MAX || vc6 > UINT8_MAX) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: subnet counters exceed wire limit v4=%d v6=%d", vc, vc6); goto fail; + } } + ni = topo_node_registry_find(groups, instance->node_id); + changed = !old || !ni || strcmp(ni->node_name ? ni->node_name : "", instance->name) || + memcmp(ni->public_key, instance->my_keys.public_key, SC_PUBKEY_SIZE) || + memcmp(ni->ed25519_public_key, instance->my_ed25519_pubkey, SC_PUBKEY_SIZE) || + ni->client_type != instance->client_type || ni->client_activity != instance->client_activity || + old->radio != lq->radio || old->phone_status != lq->phone_status; + if (!changed) { + struct TOPO_SUBNET4* a4 = old->subnets ? old->subnets->v4_subnets : NULL; + struct TOPO_SUBNET4* b4 = lq->subnets ? lq->subnets->v4_subnets : NULL; + while (a4 && b4 && a4->prefix_length == b4->prefix_length && !memcmp(a4->addr, b4->addr, 4)) { a4 = a4->next; b4 = b4->next; } + struct TOPO_SUBNET6* a6 = old->subnets ? old->subnets->v6_subnets : NULL; + struct TOPO_SUBNET6* b6 = lq->subnets ? lq->subnets->v6_subnets : NULL; + while (a6 && b6 && a6->prefix_length == b6->prefix_length && !memcmp(a6->addr, b6->addr, 16)) { a6 = a6->next; b6 = b6->next; } + changed = a4 || b4 || a6 || b6; + } + if (!changed) { + old->last_ver = ni->ver; old->last_timestamp = ni->timestamp; + DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "local node unchanged: group=%016llx ver=%u", (unsigned long long)group->group_id, ni->ver); + goto unchanged; + } + name = u_strdup(instance->name); + if (!name) goto allocation_failed; + if (!ni) { + ni = u_calloc(1, sizeof(*ni)); new_identity = 1; + if (!ni) goto allocation_failed; + } else { topo_node_registry_ref(groups, instance->node_id); retained = 1; } + struct TOPO_NODE candidate = *ni; + candidate.node_id = instance->node_id; candidate.ver = (ni->ver % 255) + 1; + candidate.node_name = name; candidate.client_type = instance->client_type; candidate.client_activity = instance->client_activity; + memcpy(candidate.public_key, instance->my_keys.public_key, SC_PUBKEY_SIZE); + memcpy(candidate.ed25519_public_key, instance->my_ed25519_pubkey, SC_PUBKEY_SIZE); + if (group->group_type != TOPO_GROUP_TYPE_CHAT && instance->rt) { + routes_changed = 1; + route_delete(instance->rt, old); + if (vc && !route_insert(instance->rt, lq)) { + DEBUG_ERROR(DEBUG_CATEGORY_ROUTING, "local node update: route conflict/allocation group=%016llx", + (unsigned long long)group->group_id); goto fail; + } + } + if (topo_node_sign_self(instance, &candidate) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: cannot publish group=%016llx", (unsigned long long)group->group_id); goto fail; + } + char* old_name = ni->node_name; + *ni = candidate; name = NULL; u_free(old_name); + if (new_identity) { + struct TOPO_NODE* stored = topo_node_registry_store(groups, ni); + if (!stored) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: cannot register identity"); goto fail; } + ni = stored; new_identity = 0; retained = 1; + } + lq->last_ver = ni->ver; lq->last_timestamp = ni->timestamp; + if (old) { topo_nodeq_clear_paths(old); topo_nodeq_free_group_fields(groups, old); u_free(old); } + group->local_node = lq; + DEBUG_INFO(DEBUG_CATEGORY_BGP, "local node published: group=%016llx v4=%d v6=%d ver=%u timestamp=%llu", + (unsigned long long)group->group_id, vc, vc6, ni->ver, (unsigned long long)ni->timestamp); return vc; +allocation_failed: + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: preparation allocation failed group=%016llx", + (unsigned long long)group->group_id); +fail: + if (routes_changed) { + route_delete(instance->rt, lq); + if (old && old->subnets && old->subnets->v4_subnets && !route_insert(instance->rt, old)) + DEBUG_ERROR(DEBUG_CATEGORY_ROUTING, "local node update: previous routes restoration failed group=%016llx", + (unsigned long long)group->group_id); + } + if (new_identity) topo_node_destroy(groups, ni); + if (retained) topo_node_registry_unref(groups, instance->node_id); + u_free(name); +unchanged: + if (lq->subnets) { + free_v4_sub_list(groups->v4_subnet_pool, lq->subnets->v4_subnets); + free_v6_sub_list(groups->v6_subnet_pool, lq->subnets->v6_subnets); + u_free(lq->subnets); + } + u_free(lq); + return changed ? -1 : vc; } /* Пересобирает списки адресов своего узла из ETCP-сокетов; при изменении — подпись, БД, рассылка. */ diff --git a/src/routing_layer/topo_node.h b/src/routing_layer/topo_node.h index 64cdb9a2..d0fd382c 100644 --- a/src/routing_layer/topo_node.h +++ b/src/routing_layer/topo_node.h @@ -130,6 +130,9 @@ struct TOPOMSG_REALITY_SOCK { uint8_t server_name[TOPO_REALITY_SNAME_MAX]; } __attribute__((packed)); +/* NODEINFO: этот заголовок, имя без NUL, SOCKMETA4/ADDR4/SOCKMETA6/ADDR6/REALITY, + * опциональные SUBNET4/SUBNET6, затем hop_count ID. Счётчики — число элементов, имя до 63 байт. + * Многобайтные числа wire копируются в нативном порядке; IP — сетевые байты, port в памяти — host order. */ struct TOPOMSG_NODE { uint8_t flags; uint64_t group_id, node_id; uint8_t ver; uint64_t timestamp; // версия всей подписанной записи, Unix microseconds @@ -206,13 +209,13 @@ struct TOPO_NODEPATH { uint64_t exchange; // local_epoch сессии, в которой путь подтверждён NODEINFO }; -/** Узел в контексте группы (per-group данные). Хранится в TOPO_GROUP->nodes (ll_queue). */ +/** Узел группы: удалённый хранится в TOPO_GROUP->nodes (ll_queue), локальный — отдельно в local_node. */ struct TOPO_GROUP_NODE { struct ll_entry ll; // queue entry, хеш-ключ 8 байт → node_id uint64_t node_id; // идентификатор узла в этой группе struct TOPO_NODESUBNETS* subnets; // подсети узла (v4/v6), для route_insert() struct ll_queue* paths; // TOPO_NODEPATH{conn,hop_count,rtt} — маршруты - uint8_t last_ver; // последняя версия NODEINFO (защита от stale, BGP) + uint8_t last_ver; // диагностический циклический ver; свежесть определяется timestamp uint64_t last_timestamp; // последняя принятая версия записи в группе uint8_t conn_mgr_type; // CONN_TYPE_DIRECT/REVERSE/INDIRECT/NONE uint64_t conn_mgr_intermediaries[CONN_MGR_MAX_INTERMEDIARIES]; // посредники для INDIRECT @@ -228,8 +231,10 @@ struct TOPO_GROUP_NODE { // API — глобальный реестр TOPO_NODE /* Заимствованный указатель; ref нужен, если запись должна пережить удаление из группы. */ struct TOPO_NODE* topo_node_registry_find(struct TOPO_GROUPS* groups, uint64_t node_id); -/* Принять ni и вернуть запись с одной новой ссылкой. Может вернуть существующий объект. - * При NULL входной ni остаётся у вызывающего. Подписанные записи сравниваются по timestamp. */ +/* Принять ni и вернуть запись с одной новой ссылкой. При NULL входной ni остаётся у вызывающего. + * Более свежий подписанный снимок заменяет содержимое целиком, сохраняя адрес существующего объекта. + * Старый/равный вход поглощается, возвращается прежняя запись; при двух timestamp=0 сливаются непустые bootstrap-списки. + * Новая запись может быть заменена более свежим снимком SQLite перед регистрацией. */ struct TOPO_NODE* topo_node_registry_store(struct TOPO_GROUPS* groups, struct TOPO_NODE* ni); /* Добавить/снять ссылку; последняя unref удаляет запись из реестра и освобождает её. */ void topo_node_registry_ref(struct TOPO_GROUPS* groups, uint64_t node_id); @@ -244,17 +249,23 @@ struct TOPO_GROUP_NODE* topo_node_find_by_id(struct TOPO_GROUP* group, uint64_t void topo_nodeq_free_group_fields(struct TOPO_GROUPS* groups, struct TOPO_GROUP_NODE* nq); /* Освободить принадлежащие узлу записи путей и их контейнер; ETCP-соединения заимствованы. */ void topo_nodeq_clear_paths(struct TOPO_GROUP_NODE* nq); -/* Полное удаление узла из группы: отменяет влётные пробы, освобождает paths/subnets, - * убирает из group->nodes и освобождает сам nq. Единственный корректный способ удаления узла. */ +/* Полное удаление узла из group->nodes: отменяет пробы, освобождает paths/subnets, + * убирает из очереди и освобождает nq. local_node освобождает владелец группы отдельно. */ void topo_nodeq_remove_node(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq); -/* Размер переменной части NODEINFO по счётчикам заголовка. */ +/* Размер переменной части NODEINFO или -1 при NULL/счётчиках подсетей без TOPO_FLAG_SEND_SUBNETS. */ int topo_node_dyn_size(const struct TOPOMSG_NODE* msg); -/* Собрать NODEINFO в out; длина или -1. Входные структуры остаются у вызывающего. */ +/* Собрать NODEINFO в out[out_max]; длина или -1. Входные структуры остаются у вызывающего. + * Без TOPO_FLAG_SEND_SUBNETS обе секции подсетей отсутствуют, их счётчики равны 0. */ int topo_node_serialize(struct TOPO_NODE* ni, struct TOPO_GROUP_NODE* nq, uint64_t group_id, uint8_t flags, uint8_t* out, size_t out_max, uint16_t cumulative_rtt); -/* Разобрать NODEINFO; 0/-1. Выходные объекты выделены отдельно и передаются вызывающему. */ +/* Разобрать ровно len байт NODEINFO; 0/-1. group->instance->topo_groups содержит пулы списков. + * out_ni обязателен, остальные выходы независимы и необязательны. Отсутствие выхода не меняет обход/проверки секций. + * Перед разбором все переданные выходы обнуляются, прежние объекты не освобождаются. + * При успехе node уничтожается topo_node_destroy, подсети возвращаются в пулы groups, hops освобождается u_free. + * Пустые секции дают NULL/0; при ошибке выходы остаются NULL/0, частичные объекты освобождены. + * Проверяет размер, профили и /0..32, /0..128; подпись и допустимость BGP-пути проверяет вызывающий. */ int topo_node_deserialize(struct TOPO_GROUP* group, const uint8_t* data, size_t len, struct TOPO_NODE** out_ni, struct TOPO_NODESUBNETS** out_subnets, uint64_t** out_hop_list, uint8_t* out_hop_count, uint16_t* out_cumulative_rtt); @@ -265,15 +276,24 @@ uint64_t* topo_node_best_hop_list(struct TOPO_GROUP_NODE* nq, uint8_t* out_count /* Лучший живой путь: минимум переходов, затем ID соседа. Указатель заимствованный. */ struct TOPO_NODEPATH* topo_node_best_path(const struct TOPO_GROUP_NODE* nq); -/* Обновить локальный узел группы и его анонс. 0/-1. */ +/* Подготовить и опубликовать local_node с актуальными подсетями/состоянием; число IPv4-подсетей или -1. + * Учитывает адреса и префиксы, а не только счётчики; CHAT не анонсирует подсети и возвращает 0. + * При отказе подготовки/подписи или конфликте IPv4 сохраняет прежние local_node и маршруты. + * Вызывает обновление общей подписанной записи; рассылку NODEINFO выполняет вызывающий. */ int topo_group_update_my_nodeinfo(struct UTUN_INSTANCE* instance, struct TOPO_GROUP* group); /* Создать подписанную локальную идентичность с отдельной ссылкой ядра. 0/-1. */ int topo_node_init_self(struct UTUN_INSTANCE* instance); -int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance); /* 1=changed, 0=unchanged, -1=error */ +/* Пересобрать общие адреса из рабочих UDP/TCP-сокетов; 1=изменились, 0=прежние, -1=ошибка. + * Новая запись подписывается/сохраняется перед заменой, затем рассылается во все группы. + * Ошибка SQL-проекции даёт -1 и после применённого изменения; при неизменных адресах проекция также обновляется. */ +int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance); /* Обработчик изменений локального сокета: обновляет адреса узла. */ void topo_node_on_socket_changed(struct ETCP_SOCKET* sock, int event, void* arg); -/* Вывести диагностику узлов группы в лог / текстовый буфер. */ +/* Вывести group->nodes в лог категории BGP на уровне INFO; отдельный local_node не добавляется. */ void topo_node_dump_all(struct TOPO_GROUP* group); +/* Записать дамп в buf: длина без NUL или -1 при ошибке форматирования. Ёмкость ограничена INT_MAX. + * При усечении завершает запись сразу, заменяет хвост доступной частью "\n[TRUNCATED]\n" и возвращает реальную длину. + * При buf != NULL и buf_size > 0 строка всегда завершена NUL. Некорректные аргументы дают 0 и пустую строку, если есть буфер. */ int topo_node_format_all(struct TOPO_GROUP* group, char* buf, size_t buf_size); /* 1, если хотя бы в одной группе пора повторить ping этого узла; иначе 0. */ int topo_node_ping_request_cbk(struct TOPO_GROUPS* groups, uint64_t node_id); @@ -286,15 +306,18 @@ uint16_t topo_get_chain_rtt(struct TOPO_GROUP_NODE* nq); /* Лимиты буфера подписываемых данных и полного сетевого NODEINFO. */ #define TOPO_SIG_MSG_MAX_SIZE 8192 #define TOPO_NODE_WIRE_MAX_SIZE 16384 -/* Канонические байты подписи: node_id, ver, timestamp, X25519-ключ, имя, - * тип/активность и полные списки сокетов/адресов/Reality. Длина или -1. */ +/* Канонические байты: node_id(native), ver, timestamp(BE), X25519-ключ, имя до 63 байт + NUL, + * тип/активность, пять счётчиков, SOCKMETA4/ADDR4/SOCKMETA6/ADDR6/REALITY. Порты здесь LE, SNI с NUL. + * Групповые flags/id, подсети, hops и RTT не подписываются. Длина или -1, буфер при ошибке может быть заполнен частично. */ int topo_node_build_sig_msg(struct TOPO_NODE* ni, uint8_t* buf, size_t buf_size); -/* Увеличить timestamp, подписать локальный NODEINFO и сохранить снимок в SQLite. 0/-1. */ +/* Увеличить timestamp (выше текущего/сохранённого), подписать локальный NODEINFO; при наличии SQLite сохранить снимок. + * 0/-1; при ошибке возвращает прежние timestamp/подпись, остальные изменённые вызывающим поля не откатывает. */ int topo_node_sign_self(struct UTUN_INSTANCE* instance, struct TOPO_NODE* ni); -/* Проверить timestamp, привязку node_id к ключу и подпись. 0/-1. */ +/* Проверить 0 < timestamp <= INT64_MAX, node_id=derive(X25519) и подпись предоставленным Ed25519-ключом. 0/-1. */ int topo_node_verify(const struct TOPO_NODE* ni); +/* Заимствованные списки: ni обязателен; подсетевые аксессоры допускают r=NULL. */ static inline const struct TOPO_SOCKMETA4* topo_v4_sock_meta(const struct TOPO_NODE* ni) { return ni->v4_sock_meta; } static inline const struct TOPO_ADDR4* topo_v4_addrs(const struct TOPO_NODE* ni) { return ni->v4_addrs; } static inline const struct TOPO_SOCKMETA6* topo_v6_sock_meta(const struct TOPO_NODE* ni) { return ni->v6_sock_meta; } diff --git a/src/routing_layer/topo_node_doc.md b/src/routing_layer/topo_node_doc.md index b57c8a37..292427ab 100644 --- a/src/routing_layer/topo_node_doc.md +++ b/src/routing_layer/topo_node_doc.md @@ -1,178 +1,152 @@ -# topo_node +# topo_node — модель, снимок и локальное состояние узла -## 1. Назначение +## Два уровня записи -Модель данных узла топологии — структуры для представления узла в памяти, сериализации/ -десериализации в бинарный wire-формат для BGP-обмена, глобальный реестр идентичностей -(refcounting) и диагностический дамп. +TOPO_NODE содержит общую идентичность: node_id, диагностический ver, timestamp, ключи, +Ed25519-подпись, имя, тип/активность, списки SOCKMETA4/ADDR4/SOCKMETA6/ADDR6/REALITY. +Групповых flags, group_id, подсетей и путей в нём нет. -Модуль определяет два слоя: -- **Память:** связанные списки (`TOPO_ADDR4`, `TOPO_SOCKMETA4`, `TOPO_SUBNET4` и др.) и две - структуры узла — `TOPO_NODE` (глобальная идентичность) и `TOPO_GROUP_NODE` (per-group данные). -- **Wire-формат:** packed-структуры (`TOPOMSG_NODE`, `TOPOMSG_ADDR4` и др.) для передачи по сети. +TOPO_GROUP_NODE содержит node_id, подсети, пути, last_ver/last_timestamp, состояние conn_mgr, +пробы связности, presence/up, radio/phone_status и заимствованный NCD handle. +Удалённые записи находятся в group->nodes; локальная запись хранится отдельно в group->local_node. +TOPO_NODEPATH содержит заимствованный ETCP_CONN, hop_count, cumulative_rtt, exchange +и массив uint64_t[hop_count] непосредственно после структуры. -Сам модуль не содержит сетевой логики BGP-обмена — этим занимается `topo_group`. Здесь только -данные и их упаковка/распаковка. Персистентность — в `topo_node_sqlite`. +Все операции выполняются в uasync-потоке ядра. BGP-сессией/обменом управляет topo_group, +хранением в общей SQLite — topo_node_sqlite. Этот модуль также готовит локальную запись, +обновляет адреса и инициирует рассылку после их изменения. -Два уровня представления узла: -- **TOPO_NODE** — глобальная идентичность (pubkeys, имя, адреса, self-signature). Хранится один - раз в глобальном реестре `TOPO_GROUPS->node_registry` (hash по node_id), разделяется между - группами через `group_ref_count`. -- **TOPO_GROUP_NODE** — узел в контексте группы: `node_id`, `paths`, `subnets`, `last_ver`, - `connectivity`, `conn_presence/conn_up`, `handle`. Хранится в `TOPO_GROUP->nodes`. +## Реестр и память -## 2. Как пользоваться +node_registry хранит отдельный ll_entry с node_id и указателем TOPO_NODE. +registry_find возвращает заимствованный указатель. Для удержания записи берётся registry_ref; +последний registry_unref удаляет registry entry, списки, имя и сам объект. -### Типовые сценарии +registry_store принимает входной объект при успехе и возвращает запись с одной новой ссылкой: -**Сборка информации о себе:** -```c -topo_group_update_my_nodeinfo(instance, group); -``` -Строит `group->local_node` (`TOPO_GROUP_NODE`) из конфига: сокеты, адреса (interface/NAT/real), -подсети; при изменениях инкрементирует `ver`. Для chat-групп (`TOPO_GROUP_TYPE_CHAT`) подсети -не включаются. +- Более свежий подписанный timestamp заменяет всю запись, включая пустые списки, но адрес существующего объекта сохраняется. +- Старый/равный вход освобождается, возвращается прежняя запись. Конфликт одинаковых timestamp логируется. +- Для двух bootstrap-записей с timestamp=0 сливаются присутствующие списки; полноценная идентичность может обновить скалярные поля. +- При новой регистрации более свежий/равный сохранённый снимок SQLite может заменить вход. +- При NULL-результате вход остаётся у вызывающего. -**Сериализация для отправки по BGP:** -```c -struct TOPO_NODE* ni = topo_node_registry_find(groups, nq->node_id); -uint8_t buf[4096]; -int len = topo_node_serialize(ni, nq, group_id, flags, buf, sizeof(buf), cumulative_rtt); -``` -Запаковывает `TOPO_NODE` + per-group данные (`TOPO_GROUP_NODE`) в буфер: заголовок -`TOPOMSG_NODE`, затем имя узла, v4/v6 sock_meta, v4/v6 адреса, подсети (если -`TOPO_FLAG_SEND_SUBNETS`) и hop-лист (через `topo_node_best_hop_list`). +У зарегистрированной записи используется registry_unref. Незарегистрированный результат parser/SQL +уничтожается topo_node_destroy(groups, ni). Списки сокетов/адресов/подсетей принадлежат пулам groups, +имя, REALITY, контейнер подсетей и отдельно возвращённый hop-list — u_malloc/u_calloc. +Для освобождения pooled-списков нужен тот же контейнер groups. -**Десериализация при получении NODEINFO:** -```c -struct TOPO_NODE* ni = NULL; -struct TOPO_NODESUBNETS* subnets = NULL; -uint64_t* hop_list = NULL; uint8_t hop_count = 0; -uint16_t cumulative_rtt = 0; -topo_node_deserialize(group, data, data_len, &ni, &subnets, &hop_list, &hop_count, &cumulative_rtt); -``` -Восстанавливает `TOPO_NODE` (через `u_calloc`), подсети и hop-лист. Освобождение: -- `topo_node_destroy(groups, ni)` — освободить `TOPO_NODE` (списки адресов + имя). -- `topo_nodeq_free_group_fields(groups, nq)` — освободить per-group поля (`subnets`, `paths`-элементы). +topo_nodeq_free_group_fields снимает ссылку реестра, освобождает подсети и обнуляет handle; +сам nq и пути остаются. topo_nodeq_clear_paths освобождает пути/контейнер, транспорт не закрывает. +Удалённый узел удаляется через topo_nodeq_remove_node: отмена проб, удаление IPv4-маршрутов, +удаление из очереди и освобождение полей/записи. local_node освобождает владелец группы отдельно. -**Поиск узла по ID:** -```c -struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(group, node_id); // per-group -struct TOPO_NODE* ni = topo_node_registry_find(groups, node_id); // глобальная идентичность -``` +## Timestamp и подпись -**Дамп всех узлов:** -```c -topo_node_dump_all(group); // в лог -topo_node_format_all(group, buf, size); // в строку -``` +timestamp — Unix microseconds, версия всей подписанной записи; 0 обозначает bootstrap. +ver — циклический счётчик 1..255, не фильтр свежести. Групповой last_timestamp хранит последнюю +принятую версию в этой группе, last_ver используется для диагностики. -### Ключевые концепции +topo_node_build_sig_msg создаёт канонические байты в таком порядке: -- **Глобальный реестр**: `TOPO_NODE` хранится один раз в `node_registry`; `topo_node_registry_store` - дедуплицирует по `node_id` (возвращает существующий при совпадении). `registry_ref/unref` - управляют `group_ref_count`; при 0 — `topo_node_destroy`. -- **Путь**: `TOPO_NODEPATH = {conn, hop_count, cumulative_rtt}` + inline `hop_list[hop_count]` - сразу после структуры. Полный hop-лист для отправки — `topo_node_best_hop_list`. -- **Self-signature**: `x25519_self_sig` — Ed25519-подпись над каноническим сообщением - (`topo_node_build_sig_msg`: pubkey || имя || client_type || client_activity || адреса). - Ставится `topo_node_sign_self`, проверяется в `topo_group_process_nodeinfo`. -- **Адреса трёх типов**: `TOPO_ADDR_INTERFACE` (LAN), `TOPO_ADDR_NAT` (после детекции), - `TOPO_ADDR_REAL` (подтверждённый прямой). Для каждого сокета — до 3 адресов. -- **TOPO_CONNECTIVITY** — локальное состояние связности (не передаётся по BGP): probe_status - (NONE/IN_PROGRESS/DONE/DEFERRED), RTT и статусы interface/nat/real, `probe_deferred_wait`. -- **TOPO_FLAG_SEND_SUBNETS** — если установлен, подсети сериализуются; для chat-групп сброшен. -- **Wire-протокол**: заголовок `TOPOMSG_NODE` (packed) + динамика: `node_name`, `SOCKMETA4*N`, - `ADDR4*N`, `SOCKMETA6*N`, `ADDR6*N`, опционально подсети, hop-лист. Размер динамики — `topo_node_dyn_size()`. +| Поле/секция | Представление подписи | +| --- | --- | +| node_id, ver | uint64 в нативном порядке, затем байт ver | +| timestamp | 8 байт BE | +| public_key | X25519, 32 байта | +| node_name | До 63 байт и NUL | +| client_type, client_activity | По одному байту | +| Счётчики | По байту: v4 sockets, v4 addrs, v6 sockets, v6 addrs, REALITY | +| SOCKMETA4, ADDR4, SOCKMETA6, ADDR6 | Полные списки в текущем порядке; адреса сетевыми байтами, порты LE | +| REALITY | socket_id, short_id[8], server_pubkey[32], version[3], SNI с NUL | -## 3. API +SOCKMETA содержит id/config_type/mapping/inbound/source, каждый по байту. +Групповые flags/id, подсети, hops и cumulative_rtt не входят в подпись. +Ed25519-ключ передаётся в записи и используется при verify; он не добавляется отдельным полем канонического сообщения. -### Основные структуры +topo_node_sign_self назначает timestamp строго выше текущего и сохранённого в SQLite, +подписывает ключом instance->my_ed25519_privkey и сохраняет снимок, если БД есть. +При ошибке восстанавливаются timestamp/подпись; другие изменённые вызывающим поля не откатываются. +Поэтому локальные обновления подписывают временный candidate до замены общей записи. -| Структура | Назначение | -|-----------|-----------| -| `struct TOPO_NODE` | Глобальная идентичность узла: `flags`, `group_id`, `node_id`, `ver`, `public_key`, `ed25519_public_key`, `x25519_self_sig`, `client_type`, `client_activity`, `node_name`, списки v4/v6 sock_meta и addrs, `group_ref_count` | -| `struct TOPO_GROUP_NODE` | Per-group запись: `node_id`, `subnets`, `paths` (queue TOPO_NODEPATH), `last_ver`, `conn_mgr_*`, `connectivity`, `conn_presence`, `conn_up`, `handle` (ncd) | -| `struct TOPO_NODEPATH` | Путь через ETCP_CONN: `{conn, hop_count, cumulative_rtt}` + inline `hop_list[]` | -| `struct TOPO_NODESUBNETS` | v4/v6 подсети узла (отдельный malloc, может быть NULL) | -| `struct TOPO_CONNECTIVITY` | Связность: probe_status, RTT, статусы interface/nat/real, probe-тайминги | -| `struct TOPOMSG_NODE` | Packed-заголовок wire-формата: счётчики, `hop_count`, `client_type/activity`, `cumulative_rtt` | -| `struct TOPOMSG_ADDR4/6` | Packed wire одного адреса (addr, port, type, socket_id, protocol) | -| `struct TOPOMSG_SOCKMETA4/6` | Packed wire сокет-метаданных (id, config_type, nat_type) | -| `struct TOPOMSG_SUBNET4/6` | Packed wire подсети (addr, prefix_length) | +topo_node_verify проверяет диапазон timestamp, node_id=derive(X25519 public_key) +и Ed25519-подпись предоставленным ключом; возвращает 0/-1. +Проверку права участия в группе и допустимости пути выполняет вызывающий BGP-слой. -### Вспомогательные типы в памяти (связанные списки) +## NODEINFO wire -| Тип | Назначение | -|-----|-----------| -| `struct TOPO_SOCKMETA4/6` | Элемент списка сокет-метаданных (next*, id, config_type, nat_type) | -| `struct TOPO_ADDR4/6` | Элемент списка адресов (next*, addr, port, type, socket_id, protocol) | -| `struct TOPO_SUBNET4/6` | Элемент списка подсетей (next*, addr, prefix_length) | +Сериализация ручная, без lib/serialize. Packed-заголовок TOPOMSG_NODE содержит поля записи, +group_id/flags, счётчики, hop_count и cumulative_rtt. Далее идут: -Элементы списков аллоцируются из memory pool'ов (`topo_groups`) через `memory_pool_alloc/free`. +| Порядок | Секция | +| --- | --- | +| 1 | node_name_len байт имени без NUL | +| 2 | SOCKMETA4, затем ADDR4 | +| 3 | SOCKMETA6, затем ADDR6 | +| 4 | REALITY с фиксированным полем server_name[256] | +| 5 | SUBNET4, затем SUBNET6, только с TOPO_FLAG_SEND_SUBNETS | +| 6 | hop_count идентификаторов uint64_t | -### Константы +Числа wire копируются в нативном порядке; IP — сетевые байты. Это представление отличается +от канонического сообщения подписи (timestamp BE, порты LE, строки с NUL). +Счётчики ограничены 255, имя — 63 байтами. dyn_size возвращает размер динамики или -1. -| Константа | Значение | Назначение | -|-----------|---------|-----------| -| `TOPO_ADDR_INTERFACE` / `TOPO_ADDR_NAT` / `TOPO_ADDR_REAL` | 0/1/2 | Тип адреса | -| `TOPO_PROTO_UDP` / `TOPO_PROTO_TCP` | 0x01/0x02 | Транспорт | -| `ADDR_TYPE_NETIF/DIRECT/NAT_EIM/NAT_STRICT` | 0/1/2/3 | Классификация адреса | -| `PROBE_STATUS_NONE/IN_PROGRESS/DONE/DEFERRED` | 0/1/2/3 | Состояние пробы | -| `PROBE_RESULT_UNKNOWN/REACHABLE/UNREACHABLE` | 0/1/2 | Результат пробы | -| `CONN_TYPE_NONE/DIRECT/REVERSE/INDIRECT` | 0/1/2/3 | Стратегия соединения | -| `CLIENT_TYPE_SERVER/DESKTOP/MOBILE` | 0/1/2 | Тип клиента | -| `CLIENT_ACTIVITY_STANDBY/ACTIVE` | 0/1 | Режим | -| `NCONN_DIRECT/INDIRECT/BGP` | 1/2/4 | Биты наличия соединений | -| `TOPO_FLAG_SEND_SUBNETS` | 0x01 | Отправлять подсети | -| `TOPO_GROUP_UTUN` | `0x8000000000000000ULL` | ID utun-группы по умолчанию | -| `TOPO_GROUP_TYPE_UTUN/CHAT` | 1/2 | Тип группы | -| `CONN_MGR_MAX_INTERMEDIARIES` | 3 | Максимум посредников | +serialize получает ni и nq, оставляет владение у вызывающего и возвращает длину либо -1. +Входные поля должны быть пригодны для публикации; serialize не проверяет подпись. +Hop-list берётся один раз из выбранного живого пути. Radio и phone_status дополняют flags. +Без TOPO_FLAG_SEND_SUBNETS счётчики подсетей равны 0; секции не записываются. -### Функции +deserialize требует group->instance->topo_groups с пулами и обязательный out_ni. +Остальные out_* независимы и необязательны. Проверяются точная длина, флаг/счётчики, +профили сокетов и IPv4 /0..32, IPv6 /0..128. Все секции обходятся и проверяются независимо +от запрошенных выходов. Подпись и допустимость BGP-пути проверяются отдельно. -**Реестр и refcounting:** -- `topo_node_registry_find(groups, node_id)` — найти `TOPO_NODE` в глобальном реестре. -- `topo_node_registry_store(groups, ni)` — сохранить с дедупликацией (возвращает существующий при совпадении). -- `topo_node_registry_ref/unref(groups, node_id)` — инкремент/декремент `group_ref_count`. -- `topo_node_destroy(groups, ni)` — освободить `TOPO_NODE` (списки адресов + имя). +Перед разбором переданные выходы обнуляются, прежние объекты не освобождаются. +Успех — 0 и атомарная выдача результата; ошибка — -1 и NULL/0 без частичных объектов. +Пустые секции дают NULL/0. ni уничтожается topo_node_destroy, подсети передаются владельцу +TOPO_GROUP_NODE, hop-list освобождается u_free. -**Per-group:** -- `topo_node_find_by_id(group, node_id)` — найти `TOPO_GROUP_NODE` по node_id (hash). -- `topo_nodeq_free_group_fields(groups, nq)` — освободить per-group поля (subnets, paths) + unref глобального TOPO_NODE. +## Локальная публикация -**Сериализация:** -- `topo_node_dyn_size(msg)` — размер динамической части по счётчикам заголовка. -- `topo_node_serialize(ni, nq, group_id, flags, out, out_max, cumulative_rtt)` — сериализовать `TOPO_NODE` + per-group в буфер. -- `topo_node_deserialize(group, data, len, &out_ni, &out_subnets, &out_hop_list, &out_hop_count, &out_cumulative_rtt)` — восстановить из wire-формата. +topo_node_init_self создаёт подписанную идентичность с отдельной ссылкой ядра; 0/-1. -**Пути:** -- `topo_node_best_hop_list(nq, &out_count, &out_rtt)` — лучший hop-лист (по min hop_count). +topo_group_update_my_nodeinfo готовит local_node из имени, ключей, типа/активности, +radio/phone_status и конфигурационных подсетей. Сравниваются адреса и префиксы, +включая замену подсети при прежнем количестве. CHAT не анонсирует подсети. -**Подпись:** -- `topo_node_build_sig_msg(ni, buf, buf_size)` — каноническое сообщение для Ed25519-подписи. -- `topo_node_sign_self(instance, ni)` — подписать свой NODEINFO (`x25519_self_sig`). +Новая запись/подсети готовятся до удаления прежнего local_node. IPv4 проходит route_insert +с запретом пересечений; при конфликте или отказе подготовки/подписи прежние узел и маршруты сохраняются. +Общая подписанная идентичность заменяется после успешного sign_self. +Возврат — число IPv4-подсетей (CHAT: 0), при ошибке -1. Рассылку запускает вызывающий. -**Обновление о себе:** -- `topo_group_update_my_nodeinfo(instance, group)` — перестроить `local_node`. -- `topo_node_update_my_addresses(instance)` — обновить адреса. -- `topo_node_on_socket_changed(sock, event, arg)` — обработчик изменений сокетов. +topo_node_update_my_addresses отдельно собирает адреса рабочих UDP/TCP-сокетов. +PRIVATE исключаются, UDP probe_only исключаются; LOCAL остаются доступными для локальных подключений. +UDP публикует интерфейс и допустимый наблюдаемый NAT endpoint, IPv6 интерфейс — отдельным списком. +TCP может публиковать внешний IP с port=0, если входящий порт неизвестен. +REALITY-параметры берутся из глобальной конфигурации и связываются с socket_id. -**Диагностика:** -- `topo_node_dump_all(group)` / `topo_node_format_all(group, buf, size)` — дамп узлов. +Изменение подписывается/сохраняется до замены списков и рассылается во все группы. +Неизменный набор не переподписывается, но SQL-проекция обновляется повторно. +Возврат 1/0/-1; ошибка проекции может дать -1 уже после применённой подписанной записи. +on_socket_changed инициирует этот пересчёт при событии сокета. -**Inline-аксессоры (.h):** -- `topo_v4_sock_meta/addr`, `topo_v6_sock_meta/addr`, `topo_v4_subnets/topo_v6_subnets`, `topo_list_count`. +## Пути, RTT и диагностика -### Зависимости +best_path выбирает путь с conn->links_up, без close_requested, по минимуму hop_count, +при равенстве — меньшему peer_node_id. READY проверяется отдельно владельцем групповой сессии. +best_hop_list возвращает заимствованный inline-массив; out_count обязателен, +out_rtt необязателен и получает RTT без локального линка. При отсутствии пути — NULL/0. -| Модуль | Использование | -|--------|--------------| -| `ll_queue.h` | `TOPO_GROUP_NODE` — запись в очереди (hash по node_id), `paths` — вложенная очередь | -| `secure_channel.h` | `SC_PUBKEY_SIZE` (32) | -| `mem.h` | `u_malloc`, `u_calloc`, `u_free` | -| `memory_pool.h` | Элементы списков из пулов `topo_groups` | -| `debug_config.h` | Логирование (BGP) | -| `topo_group.h` | `struct TOPO_GROUP`, `TOPO_GROUPS` | -| `route_lib.h` | `route_insert/route_delete` | -| `utun_instance.h` | `struct UTUN_INSTANCE` | +topo_get_chain_rtt складывает rtt_last локального соединения и cumulative_rtt. +UINT16_MAX обозначает неизвестный путь/локальный RTT либо насыщение суммы. +Единицы RTT и timebase-таймингов связности — 0.1 ms. + +ping_request_cbk сообщает, пора ли повторить ping хотя бы в одной группе (TTL 24 часа). +ping_update_rtt обновляет время ping и минимальные RTT во всех группах, SQL и callbacks; +сам по себе не переводит статусы достижимости в REACHABLE. + +dump_all пишет group->nodes в BGP/INFO. format_all форматирует ту же очередь в буфер: +реальная длина без NUL, -1 при ошибке форматирования, некорректные аргументы дают 0. +При наличии буфера строка завершена NUL; после первого усечения запись прекращается, +хвост заменяется доступной частью маркера TRUNCATED. Ёмкость ограничена INT_MAX. + +Полные объявления и контракты — в topo_node.h; общий жизненный цикл снимка — doc/node_snapshot.md. diff --git a/src/routing_layer/topo_node_sqlite.c b/src/routing_layer/topo_node_sqlite.c index 66261fb1..1421a6df 100644 --- a/src/routing_layer/topo_node_sqlite.c +++ b/src/routing_layer/topo_node_sqlite.c @@ -206,24 +206,35 @@ int topo_node_sqlite_member_exists(sqlite3* db, uint64_t node_id) { int topo_node_sqlite_get_member_channels(sqlite3* db, uint64_t node_id, uint64_t** out_ch_ids, int* out_count) { - if (!db || !out_ch_ids || !out_count) return -1; - *out_ch_ids = NULL; *out_count = 0; + if (out_ch_ids) *out_ch_ids = NULL; + if (out_count) *out_count = 0; + if (!db || !out_ch_ids || !out_count) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "member channels: invalid arguments"); return -1; } sqlite3_stmt* s = NULL; if (sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers\\_%' ESCAPE '\\'", - -1, &s, NULL) != SQLITE_OK) return -1; + -1, &s, NULL) != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "member channels: enumerate tables: %s", sqlite3_errmsg(db)); return -1; + } uint64_t* ids = NULL; int count = 0, cap = 0; char sql[256]; - while (sqlite3_step(s) == SQLITE_ROW) { + int rc; + while ((rc = sqlite3_step(s)) == SQLITE_ROW) { const char* tbl = (const char*)sqlite3_column_text(s, 0); if (!tbl || strncmp(tbl, "peers_", 6) != 0) continue; snprintf(sql, sizeof(sql), "SELECT 1 FROM \"%s\" WHERE node_id=?", tbl); sqlite3_stmt* m = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &m, NULL) == SQLITE_OK) { - sqlite3_bind_int64(m, 1, (sqlite3_int64)node_id); - int is_member = (sqlite3_step(m) == SQLITE_ROW); + int member_rc = sqlite3_prepare_v2(db, sql, -1, &m, NULL); + if (member_rc == SQLITE_OK) member_rc = sqlite3_bind_int64(m, 1, (sqlite3_int64)node_id); + if (member_rc == SQLITE_OK) member_rc = sqlite3_step(m); + if (member_rc != SQLITE_ROW && member_rc != SQLITE_DONE) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "member channels: table=%s node=%016llx rc=%d: %s", + tbl, (unsigned long long)node_id, member_rc, sqlite3_errmsg(db)); + sqlite3_finalize(m); u_free(ids); sqlite3_finalize(s); return -1; + } + { + int is_member = member_rc == SQLITE_ROW; sqlite3_finalize(m); if (is_member) { uint64_t ch = strtoull(tbl + 6, NULL, 10); @@ -231,7 +242,10 @@ int topo_node_sqlite_get_member_channels(sqlite3* db, uint64_t node_id, if (count == cap) { cap = cap ? cap * 2 : 8; uint64_t* tmp = u_realloc(ids, (size_t)cap * sizeof(uint64_t)); - if (!tmp) { u_free(ids); sqlite3_finalize(s); return -1; } + if (!tmp) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "member channels: allocation failed count=%d", count); + u_free(ids); sqlite3_finalize(s); return -1; + } ids = tmp; } ids[count++] = ch; @@ -239,6 +253,11 @@ int topo_node_sqlite_get_member_channels(sqlite3* db, uint64_t node_id, } } } + if (rc != SQLITE_DONE) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "member channels: enumeration failed node=%016llx rc=%d: %s", + (unsigned long long)node_id, rc, sqlite3_errmsg(db)); + u_free(ids); sqlite3_finalize(s); return -1; + } sqlite3_finalize(s); *out_ch_ids = ids; *out_count = count; return 0; @@ -586,44 +605,47 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id, return rc == SQLITE_DONE ? 0 : -1; } +/* Удаляет канал и производные таблицы целиком; savepoint сохраняет внешнюю транзакцию. */ int topo_node_sqlite_channel_delete(sqlite3* db, const char* channel_id) { - if (!db || !channel_id || !channel_id[0]) return -1; - - char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); - char msg_tbl[80]; sanitize_ch_id(channel_id, msg_tbl, sizeof(msg_tbl)); - /* msg_ */ - char msg_full[80]; snprintf(msg_full, sizeof(msg_full), "msg_%s", msg_tbl); - - /* 1. drop per-channel tables (индексы дропятся вместе с таблицей) */ - char sql[128]; - snprintf(sql, sizeof(sql), "DROP TABLE IF EXISTS \"%s\"", peers_tbl); - sqlite3_exec(db, sql, NULL, NULL, NULL); - snprintf(sql, sizeof(sql), "DROP TABLE IF EXISTS \"%s\"", msg_full); - sqlite3_exec(db, sql, NULL, NULL, NULL); - snprintf(sql, sizeof(sql), "DROP TABLE IF EXISTS \"feedback_%s\"", msg_tbl); - if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: delete channel=%s: %s", channel_id, sqlite3_errmsg(db)); return -1; + if (!db || !channel_id || !channel_id[0]) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "channel delete: invalid arguments"); return -1; + } + int rc = sqlite3_exec(db, "SAVEPOINT delete_topo_channel", NULL, NULL, NULL); + if (rc != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "channel delete savepoint: channel=%s rc=%d: %s", channel_id, rc, sqlite3_errmsg(db)); return -1; } - - /* 2. delete channels row (вместе с канальными privkey) */ sqlite3_stmt* st = NULL; - if (sqlite3_prepare_v2(db, "DELETE FROM channels WHERE channel_id=?", -1, &st, NULL) == SQLITE_OK) { - sqlite3_bind_text(st, 1, channel_id, -1, SQLITE_STATIC); - sqlite3_step(st); - sqlite3_finalize(st); + char san[64], sql[512]; sanitize_ch_id(channel_id, san, sizeof(san)); + snprintf(sql, sizeof(sql), "DROP TABLE IF EXISTS \"peers_%s\"; DROP TABLE IF EXISTS \"msg_%s\";" + " DROP TABLE IF EXISTS \"feedback_%s\"", san, san, san); + if ((rc = sqlite3_exec(db, sql, NULL, NULL, NULL)) != SQLITE_OK) goto fail; + rc = sqlite3_prepare_v2(db, "DELETE FROM channels WHERE channel_id=?", -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, channel_id, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + if (rc != SQLITE_DONE) goto fail; + sqlite3_finalize(st); st = NULL; + rc = sqlite3_prepare_v2(db, "SELECT 1 FROM sqlite_master WHERE type='table' AND name='merkle_tree_hash'", -1, &st, NULL); + if (rc != SQLITE_OK) goto fail; + rc = sqlite3_step(st); + if (rc != SQLITE_ROW && rc != SQLITE_DONE) goto fail; + int has_merkle = rc == SQLITE_ROW; + sqlite3_finalize(st); st = NULL; + if (has_merkle) { + rc = sqlite3_prepare_v2(db, "DELETE FROM merkle_tree_hash WHERE substr(namespace,instr(namespace,':')+1)=?", -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, channel_id, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + if (rc != SQLITE_DONE) goto fail; + sqlite3_finalize(st); st = NULL; } - - /* 3. delete merkle tree hashes for this namespace */ - sqlite3_stmt* mh = NULL; - if (sqlite3_prepare_v2(db, "DELETE FROM merkle_tree_hash WHERE substr(namespace,instr(namespace,':')+1)=?", -1, &mh, NULL) == SQLITE_OK) { - sqlite3_bind_text(mh, 1, channel_id, -1, SQLITE_STATIC); - sqlite3_step(mh); - sqlite3_finalize(mh); - } - - DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_node_sqlite_channel_delete: ch=%s peers=%s msg=%s", - channel_id, peers_tbl, msg_full); + if ((rc = sqlite3_exec(db, "RELEASE delete_topo_channel", NULL, NULL, NULL)) != SQLITE_OK) goto fail; + DEBUG_INFO(DEBUG_CATEGORY_BGP, "channel deleted: channel=%s tables=peers/msg/feedback merkle=%d", channel_id, has_merkle); return 0; +fail: + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "channel delete failed: channel=%s rc=%d: %s", channel_id, rc, sqlite3_errmsg(db)); + sqlite3_finalize(st); + if (sqlite3_exec(db, "ROLLBACK TO delete_topo_channel; RELEASE delete_topo_channel", NULL, NULL, NULL) != SQLITE_OK) + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "channel delete rollback failed: channel=%s: %s", channel_id, sqlite3_errmsg(db)); + return -1; } /* Упрощённый плейсхолдер мембера от topo_group: только node_id + pubkeys, source=1. @@ -724,11 +746,16 @@ int topo_node_sqlite_member_owner_put(sqlite3* db, const char* channel_id, uint6 return 0; } +/* Записывает уже проверенную вызывающим идентичность; любой отказ SQL возвращается вызывающему. */ int topo_node_sqlite_node_update_verified(sqlite3* db, uint64_t node_id, const char* name, const uint8_t* x25519, const uint8_t* ed25519, uint64_t update_ts, time_t created_at) { - if (!db || !x25519 || !ed25519) return -1; + if (!db || !x25519 || !ed25519) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "verified node write: invalid arguments node=%016llx", (unsigned long long)node_id); + return -1; + } sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, + const char* stage = "prepare"; + int rc = sqlite3_prepare_v2(db, "INSERT INTO nodes(node_id, name, x25519_pubkey, ed25519_pubkey, created_at, update_ts, online)" " VALUES(?,?,?,?,?,?,1)" " ON CONFLICT(node_id) DO UPDATE SET" @@ -739,15 +766,25 @@ int topo_node_sqlite_node_update_verified(sqlite3* db, uint64_t node_id, " created_at=COALESCE(nodes.created_at, excluded.created_at)," " update_ts=CASE WHEN nodes.update_ts IS NULL OR excluded.update_ts > nodes.update_ts" " THEN excluded.update_ts ELSE nodes.update_ts END", - -1, &stmt, NULL) != SQLITE_OK) return -1; - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); - sqlite3_bind_text(stmt, 2, name && name[0] ? name : "", -1, SQLITE_STATIC); - sqlite3_bind_blob(stmt, 3, x25519, 32, SQLITE_STATIC); - sqlite3_bind_blob(stmt, 4, ed25519, 32, SQLITE_STATIC); - sqlite3_bind_int64(stmt, 5, (sqlite3_int64)created_at); - sqlite3_bind_int64(stmt, 6, (sqlite3_int64)update_ts); - sqlite3_step(stmt); - sqlite3_finalize(stmt); + -1, &stmt, NULL); + if (rc == SQLITE_OK) { + stage = "bind"; + rc = sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(stmt, 2, name && name[0] ? name : "", -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_blob(stmt, 3, x25519, 32, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_blob(stmt, 4, ed25519, 32, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(stmt, 5, (sqlite3_int64)created_at); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(stmt, 6, (sqlite3_int64)update_ts); + } + if (rc == SQLITE_OK) { stage = "step"; rc = sqlite3_step(stmt); } + if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "verified node write: node=%016llx stage=%s rc=%d: %s", + (unsigned long long)node_id, stage, rc, sqlite3_errmsg(db)); + int final_rc = sqlite3_finalize(stmt); + if (final_rc != SQLITE_OK && rc == SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, + "verified node write: node=%016llx stage=finalize rc=%d: %s", (unsigned long long)node_id, final_rc, sqlite3_errmsg(db)); + if (rc != SQLITE_DONE || final_rc != SQLITE_OK) return -1; + DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "verified node stored: node=%016llx timestamp=%llu changed=%d", + (unsigned long long)node_id, (unsigned long long)update_ts, sqlite3_changes(db)); return 0; } @@ -763,7 +800,10 @@ int topo_node_sqlite_member_get_join(sqlite3* db, const char* channel_id, uint64 int rc = -1; if (sqlite3_step(stmt) == SQLITE_ROW) { const uint8_t* sig = (const uint8_t*)sqlite3_column_blob(stmt, 0); - if (sig) { memcpy(join_sig_out, sig, PEERS_JOIN_SIG_SIZE); *join_ts_out = (uint64_t)sqlite3_column_int64(stmt, 1); rc = 0; } + if (sig && sqlite3_column_bytes(stmt, 0) == PEERS_JOIN_SIG_SIZE) { + memcpy(join_sig_out, sig, PEERS_JOIN_SIG_SIZE); *join_ts_out = (uint64_t)sqlite3_column_int64(stmt, 1); rc = 0; + } else DEBUG_WARN(DEBUG_CATEGORY_BGP, "invalid stored join signature channel=%s node=%016llx bytes=%d", + channel_id, (unsigned long long)node_id, sqlite3_column_bytes(stmt, 0)); } sqlite3_finalize(stmt); return rc; @@ -802,15 +842,18 @@ int topo_node_sqlite_member_in_channel(sqlite3* db, const char* channel_id, uint } int topo_node_sqlite_member_del(sqlite3* db, const char* channel_id, uint64_t node_id) { - if (!db || !channel_id) return -1; + if (!db || !channel_id) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "member delete: invalid arguments"); return -1; } char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char sql[256]; snprintf(sql, sizeof(sql), "DELETE FROM \"%s\" WHERE node_id=?", peers_tbl); sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); - sqlite3_step(stmt); sqlite3_finalize(stmt); - return 0; + int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); + if (rc == SQLITE_OK) rc = sqlite3_step(stmt); + if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "member delete: channel=%s node=%016llx rc=%d: %s", + channel_id, (unsigned long long)node_id, rc, sqlite3_errmsg(db)); + sqlite3_finalize(stmt); + return rc == SQLITE_DONE ? 0 : -1; } int topo_node_sqlite_channel_get(sqlite3* db, const char* channel_id, @@ -823,6 +866,12 @@ int topo_node_sqlite_channel_get(sqlite3* db, const char* channel_id, " FROM channels WHERE channel_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC); if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return -1; } + if ((x25519_pub && sqlite3_column_bytes(stmt, 2) != 32) || + (ed25519_pub && sqlite3_column_bytes(stmt, 3) != 32) || (signature && sqlite3_column_bytes(stmt, 4) != 64)) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid stored channel keys/signature channel=%s sizes=%d/%d/%d", channel_id, + sqlite3_column_bytes(stmt, 2), sqlite3_column_bytes(stmt, 3), sqlite3_column_bytes(stmt, 4)); + sqlite3_finalize(stmt); return -1; + } if (name_out && name_sz > 0) { const unsigned char* txt = sqlite3_column_text(stmt, 0); if (txt) snprintf(name_out, name_sz, "%s", txt); else name_out[0] = '\0'; @@ -884,14 +933,16 @@ int topo_node_sqlite_channel_set_privs(sqlite3* db, const char* channel_id, } int topo_node_sqlite_node_set_online(sqlite3* db, uint64_t node_id, int online) { - if (!db) return -1; + if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node online: database is NULL"); return -1; } sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, "UPDATE nodes SET online=? WHERE node_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1; - sqlite3_bind_int(stmt, 1, online ? 1 : 0); - sqlite3_bind_int64(stmt, 2, (sqlite3_int64)node_id); - sqlite3_step(stmt); + int rc = sqlite3_prepare_v2(db, "UPDATE nodes SET online=? WHERE node_id=?", -1, &stmt, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int(stmt, 1, online ? 1 : 0); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(stmt, 2, (sqlite3_int64)node_id); + if (rc == SQLITE_OK) rc = sqlite3_step(stmt); + if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node online: node=%016llx online=%d rc=%d: %s", + (unsigned long long)node_id, online, rc, sqlite3_errmsg(db)); sqlite3_finalize(stmt); - return 0; + return rc == SQLITE_DONE ? 0 : -1; } int topo_node_sqlite_node_get_online(sqlite3* db, uint64_t node_id) { @@ -928,75 +979,80 @@ int topo_node_sqlite_get_ed25519_pubkey(sqlite3* db, uint64_t node_id, uint8_t p return rc; } +/* Пересчитывает канальные роли/хранение/удаление; при отказе откатывает всю проекцию узла. */ int topo_node_sqlite_nodeinfo_updated(sqlite3* db, uint64_t node_id) { - if (!db) return -1; - - int best_at = -1; - { - sqlite3_stmt* s = NULL; - if (sqlite3_prepare_v2(db, - "SELECT addr_type FROM node_addresses WHERE node_id=?", -1, &s, NULL) == SQLITE_OK) { - sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id); - while (sqlite3_step(s) == SQLITE_ROW) { - int at = sqlite3_column_int(s, 0); - if (at == ADDR_TYPE_DIRECT) { best_at = ADDR_TYPE_DIRECT; break; } - if (best_at < 0 || at < best_at) best_at = at; - } - sqlite3_finalize(s); - } + if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node classification: database is NULL"); return -1; } + int rc = sqlite3_exec(db, "SAVEPOINT node_classification", NULL, NULL, NULL); + if (rc != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node classification savepoint: rc=%d: %s", rc, sqlite3_errmsg(db)); return -1; } - + sqlite3_stmt *scan = NULL, *read = NULL, *write = NULL; + const char* stage = "addresses"; + int best_at = -1; + rc = sqlite3_prepare_v2(db, "SELECT addr_type FROM node_addresses WHERE node_id=?", -1, &read, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(read, 1, (sqlite3_int64)node_id); + if (rc != SQLITE_OK) goto fail; + while ((rc = sqlite3_step(read)) == SQLITE_ROW) { + int at = sqlite3_column_int(read, 0); + if (at == ADDR_TYPE_DIRECT) { best_at = ADDR_TYPE_DIRECT; break; } + if (best_at < 0 || at < best_at) best_at = at; + } + if (rc != SQLITE_DONE && rc != SQLITE_ROW) goto fail; + sqlite3_finalize(read); read = NULL; static const int at2type[] = { [ADDR_TYPE_NETIF]=1, [ADDR_TYPE_DIRECT]=1, [ADDR_TYPE_NAT_EIM]=2, [ADDR_TYPE_NAT_STRICT]=1, [ADDR_TYPE_NAT]=2, [ADDR_TYPE_CONTACT]=2, [ADDR_TYPE_NAT_EIM_UNVERIFIED]=2 }; int base_type = best_at < 0 || (unsigned)best_at >= sizeof(at2type) / sizeof(at2type[0]) ? 0 : at2type[best_at]; if (best_at >= 0 && (unsigned)best_at >= sizeof(at2type) / sizeof(at2type[0])) DEBUG_WARN(DEBUG_CATEGORY_BGP, "invalid projected address type=%d node=%016llx", best_at, (unsigned long long)node_id); - - sqlite3_stmt* s = NULL; - if (sqlite3_prepare_v2(db, - "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers\\_%' ESCAPE '\\'", - -1, &s, NULL) != SQLITE_OK) return -1; - - char sql[256]; - while (sqlite3_step(s) == SQLITE_ROW) { - const char* tbl = (const char*)sqlite3_column_text(s, 0); - - int node_type = base_type; int is_storage = 0; int is_deleted = 0; - snprintf(sql, sizeof(sql), - "SELECT adm_tags FROM \"%s\" WHERE node_id=?", tbl); - sqlite3_stmt* a = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &a, NULL) == SQLITE_OK) { - sqlite3_bind_int64(a, 1, (sqlite3_int64)node_id); - if (sqlite3_step(a) == SQLITE_ROW) { - const char* tags = (const char*)sqlite3_column_text(a, 0); - if (tags) { - char buf[64]; - if (json_flat_get(tags, "supernode", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) node_type = 4; - if (best_at == ADDR_TYPE_DIRECT && base_type == 1) { - if (json_flat_get(tags, "storage", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_storage = 1; - } - is_deleted = adm_tags_deleted(tags); - } - } - sqlite3_finalize(a); - } - - snprintf(sql, sizeof(sql), - "UPDATE \"%s\" SET node_type=?, storage=?, deleted=? WHERE node_id=?", tbl); - sqlite3_stmt* u = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) == SQLITE_OK) { - sqlite3_bind_int(u, 1, node_type); - sqlite3_bind_int(u, 2, is_storage); - sqlite3_bind_int(u, 3, is_deleted); - sqlite3_bind_int64(u, 4, (sqlite3_int64)node_id); - sqlite3_step(u); sqlite3_finalize(u); - } + stage = "membership tables"; + rc = sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers\\_%' ESCAPE '\\'", + -1, &scan, NULL); + if (rc != SQLITE_OK) goto fail; + while ((rc = sqlite3_step(scan)) == SQLITE_ROW) { + const char* table = (const char*)sqlite3_column_text(scan, 0); + int node_type = base_type, storage = 0, deleted = 0; + char sql[256]; snprintf(sql, sizeof(sql), "SELECT adm_tags FROM \"%s\" WHERE node_id=?", table); + stage = "member tags"; + rc = sqlite3_prepare_v2(db, sql, -1, &read, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(read, 1, (sqlite3_int64)node_id); + if (rc != SQLITE_OK) goto fail; + rc = sqlite3_step(read); + if (rc == SQLITE_ROW) { + const char* tags = (const char*)sqlite3_column_text(read, 0); + char value[64]; + if (json_flat_get(tags, "supernode", value, sizeof(value)) == 0 && !strcmp(value, "yes")) node_type = 4; + if (best_at == ADDR_TYPE_DIRECT && json_flat_get(tags, "storage", value, sizeof(value)) == 0 && !strcmp(value, "yes")) + storage = 1; + deleted = adm_tags_deleted(tags); + } else if (rc != SQLITE_DONE) goto fail; + sqlite3_finalize(read); read = NULL; + snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET node_type=?,storage=?,deleted=? WHERE node_id=?", table); + stage = "member projection"; + rc = sqlite3_prepare_v2(db, sql, -1, &write, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int(write, 1, node_type); + if (rc == SQLITE_OK) rc = sqlite3_bind_int(write, 2, storage); + if (rc == SQLITE_OK) rc = sqlite3_bind_int(write, 3, deleted); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(write, 4, (sqlite3_int64)node_id); + if (rc == SQLITE_OK) rc = sqlite3_step(write); + if (rc != SQLITE_DONE) goto fail; + sqlite3_finalize(write); write = NULL; + stage = "membership tables"; } - sqlite3_finalize(s); - - DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "nodeinfo_updated: node=%016llx best_at=%d base_type=%d", + if (rc != SQLITE_DONE) goto fail; + sqlite3_finalize(scan); scan = NULL; + stage = "release"; + if ((rc = sqlite3_exec(db, "RELEASE node_classification", NULL, NULL, NULL)) != SQLITE_OK) goto fail; + DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "node classification stored: node=%016llx best_at=%d base_type=%d", (unsigned long long)node_id, best_at, base_type); return 0; +fail: + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node classification failed: node=%016llx stage=%s rc=%d: %s", + (unsigned long long)node_id, stage, rc, sqlite3_errmsg(db)); + sqlite3_finalize(write); sqlite3_finalize(read); sqlite3_finalize(scan); + if (sqlite3_exec(db, "ROLLBACK TO node_classification; RELEASE node_classification", NULL, NULL, NULL) != SQLITE_OK) + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node classification rollback failed: node=%016llx: %s", + (unsigned long long)node_id, sqlite3_errmsg(db)); + return -1; } void topo_node_sqlite_update_rtt(sqlite3* db, uint64_t node_id, uint16_t rtt) { @@ -1111,157 +1167,157 @@ struct TOPO_NODE* topo_node_sqlite_snapshot_load(sqlite3* db, struct TOPO_GROUPS } struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* groups, uint64_t node_id) { - if (!db || !groups) return NULL; + if (!db || !groups || !groups->instance || groups->instance->topo_groups != groups) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node load: invalid database/groups node=%016llx", (unsigned long long)node_id); return NULL; + } struct TOPO_NODE* snapshot = topo_node_sqlite_snapshot_load(db, groups, node_id); if (snapshot) return snapshot; + struct TOPO_NODE* ni = u_calloc(1, sizeof(*ni)); + if (!ni) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node load: allocation failed node=%016llx", (unsigned long long)node_id); return NULL; } + sqlite3_stmt* s = NULL; uint8_t pubkey[32] = {0}, ed_pubkey[32] = {0}; char* name = NULL; uint8_t client_type = 0; int found = 0; - { - sqlite3_stmt* s = NULL; - if (sqlite3_prepare_v2(db, "SELECT name,x25519_pubkey,ed25519_pubkey,client_type FROM nodes WHERE node_id=?", - -1, &s, NULL) == SQLITE_OK) { - sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id); - if (sqlite3_step(s) == SQLITE_ROW) { - const char* n = (const char*)sqlite3_column_text(s, 0); - if (n) name = u_strdup(n); - const void* pk = sqlite3_column_blob(s, 1); - if (pk && sqlite3_column_bytes(s, 1) >= 32) memcpy(pubkey, pk, 32); - const void* ep = sqlite3_column_blob(s, 2); - if (ep && sqlite3_column_bytes(s, 2) >= 32) memcpy(ed_pubkey, ep, 32); - client_type = (uint8_t)sqlite3_column_int(s, 3); - found = 1; - } - sqlite3_finalize(s); - } - } - if (!found) { u_free(name); return NULL; } - struct TOPO_ADDR4* v4_head = NULL; struct TOPO_ADDR4* v4_tail = NULL; struct TOPO_SOCKMETA4* m4_head = NULL; struct TOPO_SOCKMETA4* m4_tail = NULL; + struct TOPO_ADDR6* v6_head = NULL; struct TOPO_ADDR6* v6_tail = NULL; + struct TOPO_SOCKMETA6* m6_head = NULL; struct TOPO_SOCKMETA6* m6_tail = NULL; struct TOPO_REALITY_SOCK* r_head = NULL; struct TOPO_REALITY_SOCK* r_tail = NULL; { - sqlite3_stmt* s = NULL; + if (sqlite3_prepare_v2(db, "SELECT name,x25519_pubkey,ed25519_pubkey,client_type FROM nodes WHERE node_id=?", + -1, &s, NULL) != SQLITE_OK) goto sql_failed; + if (sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id) != SQLITE_OK) goto sql_failed; + int rc = sqlite3_step(s); + if (rc == SQLITE_ROW) { + const char* n = (const char*)sqlite3_column_text(s, 0); + if (n) { name = u_strdup(n); if (!name) goto allocation_failed; } + const void* pk = sqlite3_column_blob(s, 1); + if (pk && sqlite3_column_bytes(s, 1) >= 32) memcpy(pubkey, pk, 32); + const void* ep = sqlite3_column_blob(s, 2); + if (ep && sqlite3_column_bytes(s, 2) >= 32) memcpy(ed_pubkey, ep, 32); + client_type = (uint8_t)sqlite3_column_int(s, 3); + found = 1; + } else if (rc != SQLITE_DONE) goto sql_failed; + sqlite3_finalize(s); s = NULL; + } + if (!found) { DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "node load: missing node=%016llx", (unsigned long long)node_id); goto failed; } + + { if (sqlite3_prepare_v2(db, "SELECT address,port,addr_type,socket_id,protocol,config_type,nat_mapping,options,address_kind FROM node_addresses" " WHERE node_id=? AND family=4 ORDER BY addr_type", - -1, &s, NULL) == SQLITE_OK) { - sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id); - while (sqlite3_step(s) == SQLITE_ROW) { - const void* addr = sqlite3_column_blob(s, 0); - if (!addr || sqlite3_column_bytes(s, 0) < 4) continue; - int port = sqlite3_column_int(s, 1); - int sock_id_raw = sqlite3_column_int(s, 3); - uint8_t sock_id = (uint8_t)sock_id_raw; - uint8_t type = (uint8_t)sqlite3_column_int(s, 8); - struct TOPO_ADDR4* a4 = memory_pool_alloc(groups->v4_addr_pool); - if (!a4) continue; - memset(a4, 0, sizeof(*a4)); - int proto = sqlite3_column_int(s, 4); - memcpy(a4->addr, addr, 4); a4->port = (uint16_t)port; - a4->type = type; a4->socket_id = sock_id; a4->protocol = (uint8_t)proto; - if (!v4_head) v4_head = v4_tail = a4; - else { v4_tail->next = a4; v4_tail = a4; } + -1, &s, NULL) != SQLITE_OK) goto sql_failed; + if (sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id) != SQLITE_OK) goto sql_failed; + int rc; + while ((rc = sqlite3_step(s)) == SQLITE_ROW) { + const void* addr = sqlite3_column_blob(s, 0); + if (!addr || sqlite3_column_bytes(s, 0) < 4) continue; + int port = sqlite3_column_int(s, 1); + int sock_id_raw = sqlite3_column_int(s, 3); + uint8_t sock_id = (uint8_t)sock_id_raw; + uint8_t type = (uint8_t)sqlite3_column_int(s, 8); + struct TOPO_ADDR4* a4 = memory_pool_alloc(groups->v4_addr_pool); + if (!a4) goto allocation_failed; + memset(a4, 0, sizeof(*a4)); + int proto = sqlite3_column_int(s, 4); + memcpy(a4->addr, addr, 4); a4->port = (uint16_t)port; + a4->type = type; a4->socket_id = sock_id; a4->protocol = (uint8_t)proto; + if (!v4_head) v4_head = v4_tail = a4; + else { v4_tail->next = a4; v4_tail = a4; } - struct TOPO_SOCKMETA4* ex = m4_head; - while (ex && ex->id != sock_id) ex = ex->next; - if (!ex) { - struct TOPO_SOCKMETA4* m4 = memory_pool_alloc(groups->v4_sock_meta_pool); - if (m4) { - m4->id = sock_id; - m4->config_type = (uint8_t)sqlite3_column_int(s, 5); - /* Без подписанного snapshot профиль из SQL-проекции не считается доказательством. */ - memset(&m4->address_profile, 0, sizeof(m4->address_profile)); - m4->next = NULL; - if (!m4_head) m4_head = m4_tail = m4; - else { m4_tail->next = m4; m4_tail = m4; } - } - } - if (sqlite3_column_type(s, 7) == SQLITE_BLOB - && sqlite3_column_bytes(s, 7) == TOPO_ADDR_REALITY_OPTS_SIZE - && (proto & TOPO_PROTO_REALITY)) { - const struct TOPO_ADDR_REALITY_OPTS* o = (const struct TOPO_ADDR_REALITY_OPTS*)sqlite3_column_blob(s, 7); - struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(*r)); - if (r) { - r->socket_id = sock_id; - memcpy(r->short_id, o->short_id, 8); - memcpy(r->server_pubkey, o->server_pubkey, 32); - memcpy(r->version, o->version, 3); - memcpy(r->server_name, o->server_name, TOPO_REALITY_SNAME_MAX); - r->server_name[TOPO_REALITY_SNAME_MAX - 1] = 0; - if (!r_head) r_head = r_tail = r; - else { r_tail->next = r; r_tail = r; } - } - } + struct TOPO_SOCKMETA4* ex = m4_head; + while (ex && ex->id != sock_id) ex = ex->next; + if (!ex) { + struct TOPO_SOCKMETA4* m4 = memory_pool_alloc(groups->v4_sock_meta_pool); + if (!m4) goto allocation_failed; + m4->id = sock_id; + m4->config_type = (uint8_t)sqlite3_column_int(s, 5); + /* Без подписанного snapshot профиль из SQL-проекции не считается доказательством. */ + memset(&m4->address_profile, 0, sizeof(m4->address_profile)); + m4->next = NULL; + if (!m4_head) m4_head = m4_tail = m4; + else { m4_tail->next = m4; m4_tail = m4; } + } + if (sqlite3_column_type(s, 7) == SQLITE_BLOB + && sqlite3_column_bytes(s, 7) == TOPO_ADDR_REALITY_OPTS_SIZE + && (proto & TOPO_PROTO_REALITY)) { + const struct TOPO_ADDR_REALITY_OPTS* o = (const struct TOPO_ADDR_REALITY_OPTS*)sqlite3_column_blob(s, 7); + struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(*r)); + if (!r) goto allocation_failed; + r->socket_id = sock_id; + memcpy(r->short_id, o->short_id, 8); + memcpy(r->server_pubkey, o->server_pubkey, 32); + memcpy(r->version, o->version, 3); + memcpy(r->server_name, o->server_name, TOPO_REALITY_SNAME_MAX); + r->server_name[TOPO_REALITY_SNAME_MAX - 1] = 0; + if (!r_head) r_head = r_tail = r; + else { r_tail->next = r; r_tail = r; } } - sqlite3_finalize(s); } + if (rc != SQLITE_DONE) goto sql_failed; + sqlite3_finalize(s); s = NULL; } /* IPv6 addresses */ - struct TOPO_ADDR6* v6_head = NULL; struct TOPO_ADDR6* v6_tail = NULL; - struct TOPO_SOCKMETA6* m6_head = NULL; struct TOPO_SOCKMETA6* m6_tail = NULL; { - sqlite3_stmt* s = NULL; if (sqlite3_prepare_v2(db, "SELECT address,port,addr_type,socket_id,protocol,config_type,nat_mapping,options,address_kind FROM node_addresses" " WHERE node_id=? AND family=6 ORDER BY addr_type", - -1, &s, NULL) == SQLITE_OK) { - sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id); - while (sqlite3_step(s) == SQLITE_ROW) { - const void* addr = sqlite3_column_blob(s, 0); - if (!addr || sqlite3_column_bytes(s, 0) < 16) continue; - int port = sqlite3_column_int(s, 1); - int sock_id_raw = sqlite3_column_int(s, 3); - uint8_t sock_id = (uint8_t)sock_id_raw; - uint8_t type = (uint8_t)sqlite3_column_int(s, 8); - struct TOPO_ADDR6* a6 = memory_pool_alloc(groups->v6_addr_pool); - if (!a6) continue; - memset(a6, 0, sizeof(*a6)); - int proto6 = sqlite3_column_int(s, 4); - memcpy(a6->addr, addr, 16); a6->port = (uint16_t)port; - a6->type = type; a6->socket_id = sock_id; a6->protocol = (uint8_t)proto6; - if (!v6_head) v6_head = v6_tail = a6; - else { v6_tail->next = a6; v6_tail = a6; } + -1, &s, NULL) != SQLITE_OK) goto sql_failed; + if (sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id) != SQLITE_OK) goto sql_failed; + int rc; + while ((rc = sqlite3_step(s)) == SQLITE_ROW) { + const void* addr = sqlite3_column_blob(s, 0); + if (!addr || sqlite3_column_bytes(s, 0) < 16) continue; + int port = sqlite3_column_int(s, 1); + int sock_id_raw = sqlite3_column_int(s, 3); + uint8_t sock_id = (uint8_t)sock_id_raw; + uint8_t type = (uint8_t)sqlite3_column_int(s, 8); + struct TOPO_ADDR6* a6 = memory_pool_alloc(groups->v6_addr_pool); + if (!a6) goto allocation_failed; + memset(a6, 0, sizeof(*a6)); + int proto6 = sqlite3_column_int(s, 4); + memcpy(a6->addr, addr, 16); a6->port = (uint16_t)port; + a6->type = type; a6->socket_id = sock_id; a6->protocol = (uint8_t)proto6; + if (!v6_head) v6_head = v6_tail = a6; + else { v6_tail->next = a6; v6_tail = a6; } - struct TOPO_SOCKMETA6* ex = m6_head; - while (ex && ex->id != sock_id) ex = ex->next; - if (!ex) { - struct TOPO_SOCKMETA6* m6 = memory_pool_alloc(groups->v6_sock_meta_pool); - if (m6) { - m6->id = sock_id; - m6->config_type = (uint8_t)sqlite3_column_int(s, 5); - memset(&m6->address_profile, 0, sizeof(m6->address_profile)); - m6->next = NULL; - if (!m6_head) m6_head = m6_tail = m6; - else { m6_tail->next = m6; m6_tail = m6; } - } - } - if (sqlite3_column_type(s, 7) == SQLITE_BLOB - && sqlite3_column_bytes(s, 7) == TOPO_ADDR_REALITY_OPTS_SIZE - && (proto6 & TOPO_PROTO_REALITY)) { - const struct TOPO_ADDR_REALITY_OPTS* o = (const struct TOPO_ADDR_REALITY_OPTS*)sqlite3_column_blob(s, 7); - struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(*r)); - if (r) { - r->socket_id = sock_id; - memcpy(r->short_id, o->short_id, 8); - memcpy(r->server_pubkey, o->server_pubkey, 32); - memcpy(r->version, o->version, 3); - memcpy(r->server_name, o->server_name, TOPO_REALITY_SNAME_MAX); - r->server_name[TOPO_REALITY_SNAME_MAX - 1] = 0; - if (!r_head) r_head = r_tail = r; - else { r_tail->next = r; r_tail = r; } - } - } + struct TOPO_SOCKMETA6* ex = m6_head; + while (ex && ex->id != sock_id) ex = ex->next; + if (!ex) { + struct TOPO_SOCKMETA6* m6 = memory_pool_alloc(groups->v6_sock_meta_pool); + if (!m6) goto allocation_failed; + m6->id = sock_id; + m6->config_type = (uint8_t)sqlite3_column_int(s, 5); + memset(&m6->address_profile, 0, sizeof(m6->address_profile)); + m6->next = NULL; + if (!m6_head) m6_head = m6_tail = m6; + else { m6_tail->next = m6; m6_tail = m6; } + } + if (sqlite3_column_type(s, 7) == SQLITE_BLOB + && sqlite3_column_bytes(s, 7) == TOPO_ADDR_REALITY_OPTS_SIZE + && (proto6 & TOPO_PROTO_REALITY)) { + const struct TOPO_ADDR_REALITY_OPTS* o = (const struct TOPO_ADDR_REALITY_OPTS*)sqlite3_column_blob(s, 7); + struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(*r)); + if (!r) goto allocation_failed; + r->socket_id = sock_id; + memcpy(r->short_id, o->short_id, 8); + memcpy(r->server_pubkey, o->server_pubkey, 32); + memcpy(r->version, o->version, 3); + memcpy(r->server_name, o->server_name, TOPO_REALITY_SNAME_MAX); + r->server_name[TOPO_REALITY_SNAME_MAX - 1] = 0; + if (!r_head) r_head = r_tail = r; + else { r_tail->next = r; r_tail = r; } } - sqlite3_finalize(s); } + if (rc != SQLITE_DONE) goto sql_failed; + sqlite3_finalize(s); s = NULL; + } + if (!v4_head && !v6_head) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "node load: no bootstrap addresses node=%016llx", (unsigned long long)node_id); goto failed; } - if (!v4_head && !v6_head) { u_free(name); DEBUG_WARN(DEBUG_CATEGORY_BGP, "node_load: node 0x%016llx has no addresses (may be stub/seed — will retry when addresses arrive)", (unsigned long long)node_id); return NULL; } - struct TOPO_NODE* ni = u_calloc(1, sizeof(struct TOPO_NODE)); - if (!ni) { u_free(name); return NULL; } ni->node_id = node_id; ni->ver = 1; ni->group_ref_count = 0; ni->client_type = client_type; memcpy(ni->public_key, pubkey, 32); memcpy(ni->ed25519_public_key, ed_pubkey, 32); @@ -1277,6 +1333,18 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr topo_list_count((struct _topo_head*)v4_head), topo_list_count((struct _topo_head*)v6_head)); return ni; +sql_failed: + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node load SQL failed node=%016llx rc=%d: %s", + (unsigned long long)node_id, sqlite3_extended_errcode(db), sqlite3_errmsg(db)); + goto failed; +allocation_failed: + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node load: bootstrap allocation failed node=%016llx", (unsigned long long)node_id); +failed: + sqlite3_finalize(s); + ni->node_name = name; ni->v4_addrs = v4_head; ni->v4_sock_meta = m4_head; + ni->v6_addrs = v6_head; ni->v6_sock_meta = m6_head; ni->reality_socks = r_head; + topo_node_destroy(groups, ni); + return NULL; } int topo_node_sqlite_set_connected(sqlite3* db, const char* channel_id, uint64_t node_id, int connected) { @@ -1346,98 +1414,90 @@ int topo_node_sqlite_get_connect_peers(sqlite3* db, const char* channel_id, sqlite3_finalize(stmt); *out = peers; *out_count = count; return 0; } -int topo_node_sqlite_get_connected_peers(sqlite3* db, const char* channel_id, - uint64_t** out_ids, int* out_count) { - if (!db || !channel_id || !out_ids || !out_count) return -1; +/* Один проход SQL: частичный массив никогда не публикуется при ошибке чтения/памяти. */ +static int node_ids_query(sqlite3* db, const char* sql, int exclude_self, uint64_t self, + uint64_t** out_ids, int* out_count) { *out_ids = NULL; *out_count = 0; - char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); - char sql[192]; snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\" WHERE connected=1 AND deleted=0", peers_tbl); sqlite3_stmt* st = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1; - int cnt = 0; - while (sqlite3_step(st) == SQLITE_ROW) cnt++; - if (cnt == 0) { sqlite3_finalize(st); return 0; } - uint64_t* ids = u_malloc((size_t)cnt * sizeof(uint64_t)); - if (!ids) { sqlite3_finalize(st); return -1; } - sqlite3_reset(st); int i = 0; - while (sqlite3_step(st) == SQLITE_ROW) ids[i++] = (uint64_t)sqlite3_column_int64(st, 0); + uint64_t* ids = NULL; + size_t count = 0, capacity = 0; + int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL); + if (rc == SQLITE_OK && exclude_self) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)self); + if (rc != SQLITE_OK) goto fail; + while ((rc = sqlite3_step(st)) == SQLITE_ROW) { + if (count == capacity) { + size_t next = capacity ? capacity * 2 : 16; + if (next > UINT32_MAX / sizeof(*ids)) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "peer ID list too large: count=%zu", count); goto fail; + } + uint64_t* resized = u_realloc(ids, next * sizeof(*ids)); + if (!resized) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "peer ID list allocation failed: count=%zu", count); goto fail; } + ids = resized; capacity = next; + } + ids[count++] = (uint64_t)sqlite3_column_int64(st, 0); + } + if (rc != SQLITE_DONE) goto fail; sqlite3_finalize(st); - *out_ids = ids; *out_count = cnt; + *out_ids = ids; *out_count = (int)count; + DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "peer ID list read: count=%zu exclude_self=%d", count, exclude_self); return 0; +fail: + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "peer ID query failed: rc=%d rows=%zu sql=%s: %s", rc, count, sql, sqlite3_errmsg(db)); + sqlite3_finalize(st); u_free(ids); return -1; +} + +int topo_node_sqlite_get_connected_peers(sqlite3* db, const char* channel_id, uint64_t** out_ids, int* out_count) { + if (out_ids) *out_ids = NULL; + if (out_count) *out_count = 0; + if (!db || !channel_id || !out_ids || !out_count) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "connected peers: invalid arguments"); return -1; + } + char table[80], sql[192]; peers_table_name(channel_id, table, sizeof(table)); + snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\" WHERE connected=1 AND deleted=0", table); + return node_ids_query(db, sql, 0, 0, out_ids, out_count); } int topo_node_sqlite_get_public_peers(sqlite3* db, const char* channel_id, - uint64_t** out_ids, int* out_count, uint64_t self_node_id) { - if (!db || !channel_id || !out_ids || !out_count) return -1; - *out_ids = NULL; *out_count = 0; - char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); - char sql[384]; snprintf(sql, sizeof(sql), + uint64_t** out_ids, int* out_count, uint64_t self_node_id) { + if (out_ids) *out_ids = NULL; + if (out_count) *out_count = 0; + if (!db || !channel_id || !out_ids || !out_count) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "public peers: invalid arguments"); return -1; + } + char table[80], sql[512]; peers_table_name(channel_id, table, sizeof(table)); + snprintf(sql, sizeof(sql), "SELECT DISTINCT na.node_id FROM node_addresses na" " WHERE na.node_id!=?1 AND na.family IN (4,6) AND na.addr_type IN (1,2)" " AND EXISTS (SELECT 1 FROM nodes n WHERE n.node_id=na.node_id)" - " AND NOT EXISTS (SELECT 1 FROM \"%s\" p WHERE p.node_id=na.node_id AND p.deleted=1)", - peers_tbl); - sqlite3_stmt* st = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1; - sqlite3_bind_int64(st, 1, (sqlite3_int64)self_node_id); - int cnt = 0; - while (sqlite3_step(st) == SQLITE_ROW) cnt++; - if (cnt == 0) { sqlite3_finalize(st); return 0; } - uint64_t* ids = u_malloc((size_t)cnt * sizeof(uint64_t)); - if (!ids) { sqlite3_finalize(st); return -1; } - sqlite3_reset(st); int i = 0; - while (sqlite3_step(st) == SQLITE_ROW) ids[i++] = (uint64_t)sqlite3_column_int64(st, 0); - sqlite3_finalize(st); - *out_ids = ids; *out_count = cnt; - return 0; + " AND NOT EXISTS (SELECT 1 FROM \"%s\" p WHERE p.node_id=na.node_id AND p.deleted=1)", table); + return node_ids_query(db, sql, 1, self_node_id, out_ids, out_count); } -int topo_node_sqlite_get_supernode_peers(sqlite3* db, const char* channel_id, - uint64_t** out_ids, int* out_count) { - if (!db || !channel_id || !out_ids || !out_count) return -1; - *out_ids = NULL; *out_count = 0; - char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); - char sql[256]; snprintf(sql, sizeof(sql), - "SELECT p.node_id FROM \"%s\" p WHERE p.node_type=4 AND p.deleted=0", - peers_tbl); - sqlite3_stmt* st = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1; - int cnt = 0; - while (sqlite3_step(st) == SQLITE_ROW) cnt++; - if (cnt == 0) { sqlite3_finalize(st); return 0; } - uint64_t* ids = u_malloc((size_t)cnt * sizeof(uint64_t)); - if (!ids) { sqlite3_finalize(st); return -1; } - sqlite3_reset(st); int i = 0; - while (sqlite3_step(st) == SQLITE_ROW) ids[i++] = (uint64_t)sqlite3_column_int64(st, 0); - sqlite3_finalize(st); - *out_ids = ids; *out_count = cnt; - return 0; +int topo_node_sqlite_get_supernode_peers(sqlite3* db, const char* channel_id, uint64_t** out_ids, int* out_count) { + if (out_ids) *out_ids = NULL; + if (out_count) *out_count = 0; + if (!db || !channel_id || !out_ids || !out_count) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "supernode peers: invalid arguments"); return -1; + } + char table[80], sql[192]; peers_table_name(channel_id, table, sizeof(table)); + snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\" WHERE node_type=4 AND deleted=0", table); + return node_ids_query(db, sql, 0, 0, out_ids, out_count); } int topo_node_sqlite_get_local_peers(sqlite3* db, const char* channel_id, - uint64_t** out_ids, int* out_count, uint64_t self_node_id) { - if (!db || !channel_id || !out_ids || !out_count) return -1; - *out_ids = NULL; *out_count = 0; - char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); - char sql[384]; snprintf(sql, sizeof(sql), + uint64_t** out_ids, int* out_count, uint64_t self_node_id) { + if (out_ids) *out_ids = NULL; + if (out_count) *out_count = 0; + if (!db || !channel_id || !out_ids || !out_count) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local peers: invalid arguments"); return -1; + } + char table[80], sql[512]; peers_table_name(channel_id, table, sizeof(table)); + snprintf(sql, sizeof(sql), "SELECT DISTINCT na.node_id FROM node_addresses na" " WHERE na.node_id!=?1 AND na.family IN (4,6) AND na.addr_type IN (0,3)" " AND EXISTS (SELECT 1 FROM nodes n WHERE n.node_id=na.node_id)" - " AND NOT EXISTS (SELECT 1 FROM \"%s\" p WHERE p.node_id=na.node_id AND p.deleted=1)", - peers_tbl); - sqlite3_stmt* st = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1; - sqlite3_bind_int64(st, 1, (sqlite3_int64)self_node_id); - int cnt = 0; - while (sqlite3_step(st) == SQLITE_ROW) cnt++; - if (cnt == 0) { sqlite3_finalize(st); return 0; } - uint64_t* ids = u_malloc((size_t)cnt * sizeof(uint64_t)); - if (!ids) { sqlite3_finalize(st); return -1; } - sqlite3_reset(st); int i = 0; - while (sqlite3_step(st) == SQLITE_ROW) ids[i++] = (uint64_t)sqlite3_column_int64(st, 0); - sqlite3_finalize(st); - *out_ids = ids; *out_count = cnt; - return 0; + " AND NOT EXISTS (SELECT 1 FROM \"%s\" p WHERE p.node_id=na.node_id AND p.deleted=1)", table); + return node_ids_query(db, sql, 1, self_node_id, out_ids, out_count); } int topo_node_sqlite_get_node_type(sqlite3* db, const char* channel_id, uint64_t node_id) { diff --git a/src/routing_layer/topo_node_sqlite.h b/src/routing_layer/topo_node_sqlite.h index cc14875c..bf35595f 100644 --- a/src/routing_layer/topo_node_sqlite.h +++ b/src/routing_layer/topo_node_sqlite.h @@ -1,6 +1,7 @@ /* topo_node_sqlite — хранение подписанных снимков узлов, адресов, каналов и блоков мемберов в общей SQLite. * Модель/проверка подписей/registry — topo_node и member_sync; этот модуль реализует SQL-операции в uasync-потоке. - * snapshot_put проверяет подпись и сохраняет только более свежий timestamp; node_load предпочитает snapshot. + * Только snapshot_put/load проверяют подпись; остальные записи требуют проверенных вызывающим данных. + * snapshot_put сохраняет более свежий timestamp; node_load предпочитает snapshot. * db/groups заимствованы. Возвращённый TOPO_NODE без registry ref: передать registry либо уничтожить через topo_node_destroy. * Массивы out_ids/out_ch_ids/out_peers принадлежат вызывающему и освобождаются u_free. */ #ifndef TOPO_NODE_SQLITE_H @@ -19,41 +20,55 @@ /* flags byte in wire format */ #define PEERS_FLAG_HAS_JOIN 0x01 +/* Создать/обновить nodes, node_addresses, node_snapshots, channels; 0/-1. db открывает и закрывает вызывающий. */ int topo_node_sqlite_init(sqlite3* db); +/* Проверить и сохранить полный подписанный NODEINFO без групповых полей; старый/равный timestamp даёт 0 без замены. */ int topo_node_sqlite_snapshot_put(sqlite3* db, struct TOPO_NODE* ni); +/* Максимальный сохранённый timestamp (Unix microseconds), в том числе после инвалидирования record; 0 — нет/ошибка. */ uint64_t topo_node_sqlite_snapshot_timestamp(sqlite3* db, uint64_t node_id); +/* Незарегистрированный проверенный снимок, включая снимок без адресов; NULL — нет/невалиден/ошибка. + * groups и groups->instance->topo_groups должны обозначать один контейнер с пулами. */ struct TOPO_NODE* topo_node_sqlite_snapshot_load(sqlite3* db, struct TOPO_GROUPS* groups, uint64_t node_id); /* Персистентная запись адресов узла в node_addresses (источник истины — BGP/topo_group). - * DELETE + INSERT из ni->v4_addrs/v6_addrs; mapping/inbound/source из подписанного SOCKMETA. */ + * DELETE + INSERT из ni->v4_addrs/v6_addrs под savepoint, включая пустой набор; 0/-1, отказ откатывает замену. + * mapping/inbound/source берутся из SOCKMETA; проверку ni выполняет вызывающий. RTT при замене не сохраняется. */ int topo_node_sqlite_addrs_put(sqlite3* db, uint64_t node_id, struct TOPO_NODE* ni); -/* Есть ли у узла запись мембера хотя бы в одной peers_ (реальный мембер с ключами). */ +/* Есть ли строка хотя бы в одной peers_*: включая source=1 и deleted=1; 1/0, при ошибке также 0. */ int topo_node_sqlite_member_exists(sqlite3* db, uint64_t node_id); -/* Список channel_id (uint64) всех peers_, где узел является членом. 0=ок. */ +/* Числовые ненулевые channel_id таблиц peers_*, содержащих строку узла; включает placeholders/deleted. + * 0/-1; пустой результат NULL/0, при ошибке частичный массив не выдаётся. */ int topo_node_sqlite_get_member_channels(sqlite3* db, uint64_t node_id, uint64_t** out_ch_ids, int* out_count); +/* Создать peers_ и сохранить канал; 0/-1. Ключи — по 32, signature — 64 байта, privkey необязательны. + * При обновлении публичные ключи обязаны совпасть; имя/владелец/подпись заменяются, NULL privkey сохраняет прежний. + * Подпись/пары ключей не проверяет. Табличный суффикс: первые 63 символа, вне [A-Za-z0-9_] заменены на '_'. */ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id, const char* name, uint64_t owner_node_id, const uint8_t* x25519_pub, const uint8_t* x25519_priv, const uint8_t* ed25519_pub, const uint8_t* ed25519_priv, const uint8_t* signature); -/* Подготовить локальные поля участника при создании и загрузке канала. */ +/* Добавить radio_muted=0 в существующую peers_, если поля ещё нет; 0/-1, идемпотентно. */ int topo_node_sqlite_member_local_init(sqlite3* db, const char* channel_id); /* Полное локальное удаление канала: DROP peers_/msg_, DELETE channels, - * Удаляются feedback_ и merkle_tree_hash всех datasets этого канала. 0=ок. */ + * Удаляются feedback_ и merkle_tree_hash всех datasets канала (если таблица есть). + * 0/-1; savepoint откатывает в том числе DROP TABLE, внешняя транзакция сохраняется. */ int topo_node_sqlite_channel_delete(sqlite3* db, const char* channel_id); -/* Упрощённый плейсхолдер мембера от topo_group (source=1, не участвует в merkle-хеше). */ +/* Вставить source=1 с нулевой join_sig; существующую строку не менять. 0/-1; ключи по 32 байта. + * Плейсхолдер не участвует в Merkle-хеше; канал/peers_ должны уже существовать. */ int topo_node_sqlite_member_placeholder_put(sqlite3* db, const char* channel_id, uint64_t node_id, const uint8_t* x25519_pubkey, const uint8_t* ed25519_pubkey); /* Обновляет только блок мембера (keys/join/update_sig/update_ts/userinfo), не трогая блок владельца. - * signed_by/signature — дерево приглашений: кто подписал pubkey узла (0 = корень, ключом группы). */ + * signed_by/signature — дерево приглашений: кто подписал pubkey узла (0 = корень, ключом группы). + * 0/-1; подписи по 64, ключи по 32 байта. NULL join_sig даёт нулевую подпись, userinfo=NULL даёт ''. + * Сбрасывает source=0; свежесть/подписи проверяет вызывающий, owner/local-поля сохраняются. */ int topo_node_sqlite_member_block_put(sqlite3* db, const char* channel_id, uint64_t node_id, const uint8_t* join_sig, uint64_t join_ts, const uint8_t* update_sig, uint64_t update_ts, @@ -64,25 +79,33 @@ int topo_node_sqlite_member_block_put(sqlite3* db, const char* channel_id, uint6 int topo_node_sqlite_member_get_sign(sqlite3* db, const char* channel_id, uint64_t node_id, uint64_t* signed_by_out, uint8_t signature_out[64]); -/* Есть ли узел в таблице мемберов канала peers_ (source 0 или 1). 1=да, 0=нет. */ +/* Есть ли строка в peers_, включая placeholders/deleted. 1/0; ошибка также даёт 0. */ int topo_node_sqlite_member_in_channel(sqlite3* db, const char* channel_id, uint64_t node_id); -/* Обновляет только блок владельца (adm_tags/adm_tags_sig/storage). */ +/* Обновить owner-блок и deleted из adm_tags; 0/-1, отсутствующая строка даёт 0 без вставки. + * adm_tags/adm_tags_sig могут быть NULL; ненулевая подпись — 64 байта, проверяется вызывающим. */ int topo_node_sqlite_member_owner_put(sqlite3* db, const char* channel_id, uint64_t node_id, const char* adm_tags, const uint8_t* adm_tags_sig, int storage); +/* Прочитать ровно 64 байта join_sig и join_ts; 0/-1, выходы обязательны и при неуспехе не меняются. */ int topo_node_sqlite_member_get_join(sqlite3* db, const char* channel_id, uint64_t node_id, uint8_t* join_sig_out, uint64_t* join_ts_out); +/* Физически удалить строку участника; 0/-1. Отсутствующая строка даёт 0; tombstone/Merkle не создаёт. */ int topo_node_sqlite_member_del(sqlite3* db, const char* channel_id, uint64_t node_id); +/* Записать проверенные вызывающим ключи/имя в nodes; 0/-1. Пустое имя сохраняет прежнее при UPDATE. + * Ключи заменяются независимо от update_ts; update_ts хранит максимум, created_at сохраняется, если не NULL. + * Новая строка online=1, при UPDATE online сохраняется. Подпись и свежесть здесь не проверяются. */ int topo_node_sqlite_node_update_verified(sqlite3* db, uint64_t node_id, const char* name, const uint8_t* x25519, const uint8_t* ed25519, uint64_t update_ts, time_t created_at); +/* Метаданные канала; 0/-1. Все выходы необязательны; name_sz включает NUL, ключи по 32, подпись 64 байта. */ int topo_node_sqlite_channel_get(sqlite3* db, const char* channel_id, char* name_out, int name_sz, uint64_t* owner_node_id, uint8_t* x25519_pub, uint8_t* ed25519_pub, uint8_t* signature); +/* Прочитать первые 32 байта Ed25519 privkey; 0/-1, отсутствующий/короткий ключ даёт -1. */ int topo_node_sqlite_channel_get_priv(sqlite3* db, const char* channel_id, uint8_t* ed25519_priv_out); @@ -90,32 +113,40 @@ int topo_node_sqlite_channel_get_priv(sqlite3* db, const char* channel_id, int topo_node_sqlite_channel_get_privs(sqlite3* db, const char* channel_id, uint8_t* x25519_priv_out, uint8_t* ed25519_priv_out); -/* Записать оба канальных privkey (импорт прав админа). */ +/* Записать оба канальных privkey по 32 байта (импорт прав админа); 0/-1, без проверки пары/подписи. + * Отсутствующая строка даёт 0 без вставки; наличие канала проверяет вызывающий. */ int topo_node_sqlite_channel_set_privs(sqlite3* db, const char* channel_id, const uint8_t* x25519_priv, const uint8_t* ed25519_priv); +/* Установить online=0/1 существующей строки; 0/-1. Отсутствующая строка даёт 0 без вставки. */ int topo_node_sqlite_node_set_online(sqlite3* db, uint64_t node_id, int online); +/* online из nodes; 0, если записи нет или запрос не выполнен. */ int topo_node_sqlite_node_get_online(sqlite3* db, uint64_t node_id); /* Скопировать ровно 32 байта Ed25519 из nodes: 0 — найден, -1 — отсутствует/другая длина/ошибка SQLite. * Значение байтов не определяет наличие ключа; криптографическую пригодность проверяет операция verify. */ int topo_node_sqlite_get_ed25519_pubkey(sqlite3* db, uint64_t node_id, uint8_t pubkey_out[32]); -/** Пересчитывает node_type во всех peers_* таблицах на основе addr_type из node_addresses */ +/* Атомарно пересчитать node_type/storage/deleted во всех peers_* по адресам и adm_tags; 0/-1. + * node_type: 0 нет адресов, 1 прямой/локальный/strict, 2 NAT/contact, 4 supernode=yes. + * storage=yes действует только при наличии ADDR_TYPE_DIRECT, deleted берётся из adm_tags.deleted=yes. */ int topo_node_sqlite_nodeinfo_updated(sqlite3* db, uint64_t node_id); -/** Обновляет RTT узла: node_addresses.rtt и все peers_*.node_RTT */ +/* Обновить node_addresses.rtt и все peers_*.node_RTT, единицы 0.1 ms; ошибки логируются, обновление неатомарно. */ void topo_node_sqlite_update_rtt(sqlite3* db, uint64_t node_id, uint16_t rtt); /** * @brief Загружает TOPO_NODE из SQLite (ref_count=0). * @param groups нужен для выделения адресов из memory_pool - * @return TOPO_NODE* или NULL если узел не найден/нет адресов + * @return TOPO_NODE* или NULL, если нет пригодной записи/адресов bootstrap либо при ошибке * * Сначала загружает и проверяет подписанный snapshot; при его отсутствии собирает сведения nodes/node_addresses. - * Вызывающий передаёт владение через topo_node_registry_acquire() либо вызывает topo_node_destroy(). + * Snapshot может быть без адресов. Bootstrap требует адресов и получает timestamp=0, профиль UNKNOWN. + * При отказе SQL/памяти частичная запись освобождается. groups->instance->topo_groups должен совпадать с groups. + * Вызывающий передаёт владение через topo_node_registry_store() либо вызывает topo_node_destroy(). */ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* groups, uint64_t node_id); +/* Записать connected=0/1 существующего участника; 0/-1, без вставки отсутствующей строки. */ int topo_node_sqlite_set_connected(sqlite3* db, const char* channel_id, uint64_t node_id, int connected); /* Кандидат CHAT-подключения: роль канальная, тип устройства из подписанного snapshot. @@ -129,6 +160,10 @@ struct topo_connect_peer { * 0/-1; массив принадлежит вызывающему и освобождается u_free(). */ int topo_node_sqlite_get_connect_peers(sqlite3* db, const char* channel_id, struct topo_connect_peer** out, size_t* out_count); +/* Нижеследующие выборки: 0/-1, пусто/ошибка — NULL/0, частичный массив не выдаётся; освобождение u_free. + * connected/supernode ограничены участниками канала, self не исключают, deleted=1 исключают. + * public/local — глобальные адреса с записью nodes, исключены self и deleted=1 в указанном канале; + * наличие членства в канале не требуется. public: addr_type 1/2, local: 0/3; фильтра port!=0 нет. */ int topo_node_sqlite_get_connected_peers(sqlite3* db, const char* channel_id, uint64_t** out_ids, int* out_count); int topo_node_sqlite_get_public_peers(sqlite3* db, const char* channel_id, uint64_t** out_ids, int* out_count, uint64_t self_node_id); int topo_node_sqlite_get_local_peers(sqlite3* db, const char* channel_id, uint64_t** out_ids, int* out_count, uint64_t self_node_id); diff --git a/src/routing_layer/topo_node_sqlite_doc.md b/src/routing_layer/topo_node_sqlite_doc.md index 4c3d5b7e..f2ca7f79 100644 --- a/src/routing_layer/topo_node_sqlite_doc.md +++ b/src/routing_layer/topo_node_sqlite_doc.md @@ -1,38 +1,124 @@ -# topo_node_sqlite — SQLite-персистентность топологии +# topo_node_sqlite — хранение топологии в SQLite -## 1. Назначение -Обеспечивает сохранение и восстановление топологии P2P-сети (ноды, адреса, каналы, участники каналов) в SQLite-базе. База используется как основной источник данных для chatgui и восстановления состояния между запусками utun. +## Назначение и владение -В отличие от LMDB-слоя (`topo_node_lmdb`), который хранит данные в бинарном формате BGP-сообщений, SQLite-слой работает с нормализованными таблицами и используется для клиентских целей — чтение данных chatgui, получение списка участников канала, проверка online-статуса. +Модуль работает с общей SQLite ядра: подписанные снимки узлов, производные адреса/состояния, +описания каналов и строки участников. Базу открывает и закрывает владелец ядра; все операции +этого модуля выполняются в его uasync-потоке. Отдельного LMDB-хранилища топологии здесь нет. -## 2. Как пользоваться -1. Получить готовый `sqlite3* db` (открывается извне, обычно в `chat_sync.c`) -2. Вызвать `topo_node_sqlite_init(db)` для создания таблиц -3. Сохранять/обновлять ноды через `topo_node_sqlite_node_put()` -4. Сохранять каналы через `topo_node_sqlite_channel_put()` (автоматически создаёт таблицу `peers_`) -5. Добавлять участников в канал через `topo_node_sqlite_member_put()` -6. Читать участников из `peers_`; постраничная сетевая сериализация принадлежит `member_sync`. +Проверку подписи выполняют только topo_node_sqlite_snapshot_put/load. +Канальные метаданные, ключи участников и их версии проверяют вызывающие chat_sync/member_sync. +SQL-функции записи блоков сами не проверяют подписи и не отбрасывают старые версии. -**Нюансы:** -- Имена таблиц `peers_` санитизируются: все символы кроме `[a-zA-Z0-9_]` заменяются на `_` -- Сетевой формат участников описан в `src/chat/member_sync_doc.md`; SQLite-слой не дублирует сериализатор. -- Ключи в BLOB-полях всегда 32 байта (x25519/ed25519), подписи — 64 байта -- Функции `online` и `get_ed25519_pubkey` — простые SELECT-запросы без JOIN +Возвращённый TOPO_NODE не зарегистрирован и имеет нулевой refcount. +Передайте владение через topo_node_registry_store либо вызовите topo_node_destroy(groups, node). +Массивы идентификаторов и topo_connect_peer освобождаются через u_free. +db, groups и входные строки/объекты остаются у вызывающего. -## 3. API +## Таблицы и версии -| Функция | Описание | -|---------|----------| -| `topo_node_sqlite_init(db)` | Создаёт таблицы `nodes`, `node_addresses`, `channels` (если не существуют) | -| `topo_node_sqlite_node_put(db, nq)` | Сохраняет ноду и её адреса (IPv4/IPv6) в транзакции; старые адреса удаляются | -| `topo_node_sqlite_channel_put(...)` | Сохраняет канал в `channels` + создаёт таблицу `peers_` | -| `topo_node_sqlite_member_put(...)` | Добавляет/обновляет участника в `peers_` (INSERT OR REPLACE) | -| `topo_node_sqlite_member_get_join(db, ch_id, node_id, ...)` | Читает join_sig и join_ts участника канала | -| `topo_node_sqlite_member_del(db, ch_id, node_id)` | Удаляет участника из `peers_` | -| `topo_node_sqlite_node_update_verified(...)` | Обновляет ключи и имя ноды по условию `update_ts < новое_значение` (защита от stale-данных) | -| `topo_node_sqlite_channel_get(...)` | Читает метаданные канала (имя, владелец, ключи, подпись) | -| `topo_node_sqlite_node_set_online(db, id, online)` | Устанавливает флаг `online` для ноды | -| `topo_node_sqlite_node_get_online(db, id)` | Возвращает 1 если нода online | -| `topo_node_sqlite_get_ed25519_pubkey(db, id, out)` | Извлекает ed25519-публичный ключ ноды (32 байта) | +| Таблица | Назначение | +| --- | --- | +| node_snapshots | node_id, максимальный timestamp, полный подписанный NODEINFO в BLOB record | +| nodes | Проекция имени и ключей, online/last_seen, update_ts, created_at; не замена подписанному снимку | +| node_addresses | IPv4/IPv6 endpoints, protocol, socket_id, address_kind, NAT-профиль, классификация, RTT и REALITY options | +| channels | Имя, владелец, публичные/необязательные приватные ключи, подпись канала и локальные поля | +| peers_<id> | Блок участника, блок владельца и локальные поля конкретного канала | -**Структуры:** внешние — `sqlite3*`, `struct TOPO_GROUP_NODE`, `struct TOPO_NODE`; собственных структур нет. +topo_node_sqlite_init создаёт общие таблицы/индексы, обрабатывает прежние форматы проекции адресов +и восстанавливает deleted из adm_tags. peers_<id> создаётся при channel_put; +member_local_init идемпотентно добавляет radio_muted. + +В снимке timestamp — Unix microseconds и версия всей подписанной записи. +snapshot_put проверяет подпись и заменяет BLOB только при большем timestamp. +Старый или равный timestamp даёт успех без замены. При смене формата BLOB может инвалидироваться +без сброса сохранённого timestamp, чтобы новая локальная публикация оставалась новее. + +node_update_verified записывает уже проверенные ключи и непустое имя независимо от update_ts. +В nodes.update_ts сохраняется максимум прежнего и входного значения; это не фильтр замены ключей. +created_at сохраняется, если уже не NULL. Новая строка получает online=1, +обновление существующей строки online не меняет. + +## Снимок и bootstrap + +topo_node_sqlite_node_load сначала загружает и проверяет снимок. Валидный снимок может содержать +пустой список адресов: это полноценное опубликованное состояние, а не повод вернуть старые адреса. + +Если пригодного снимка нет, функция собирает bootstrap из nodes и node_addresses. +Для этого требуется хотя бы один адрес. Ключи/имя копируются из проекции, timestamp остаётся 0, +профили сокетов сбрасываются в UNKNOWN; сохранённая SQL-классификация не считается подписанным доказательством. +IPv4/IPv6 сохраняются отдельно, REALITY восстанавливается из options, порт 0 сохраняется как известный TCP IP. + +Пулы берутся из groups; groups->instance->topo_groups должен указывать на тот же контейнер. +При ошибке SQL или памяти частичные списки, имя, REALITY и statement освобождаются; возвращается NULL. + +## Адреса, классификация и RTT + +addrs_put заменяет все адреса узла одним savepoint: DELETE, затем INSERT IPv4/IPv6. +Пустой список очищает проекцию. При отказе замена откатывается. +Профиль mapping/inbound/source берётся из SOCKMETA, REALITY — из записи соответствующего socket_id. +Функция не проверяет подпись входного узла. Замена адресов не сохраняет прежний RTT. + +address_kind — INTERFACE/NAT/REAL, отдельно от вычисляемого addr_type. +Классификация учитывает IP, protocol, конфигурационный тип сокета и профиль доступности. +Адрес с port=0 хранится, но не является готовым endpoint для подключения. + +nodeinfo_updated под savepoint обновляет node_type, storage, deleted во всех peers_*: + +| node_type | Основание | +| --- | --- | +| 0 | Нет адресов или классификация неизвестна | +| 1 | NETIF, DIRECT или NAT_STRICT | +| 2 | NAT_EIM, NAT, CONTACT или EIM_UNVERIFIED | +| 4 | adm_tags.supernode=yes | + +При выборе адреса DIRECT имеет приоритет, иначе берётся минимальный addr_type. +storage=yes применяется только при DIRECT; deleted выводится из adm_tags.deleted=yes. +При отказе одной канальной записи весь пересчёт узла откатывается, функция возвращает -1. + +update_rtt обновляет node_addresses.rtt и peers_*.node_RTT в единицах 0.1 ms. +Обновление этих таблиц неатомарно; ошибки логируются. Online и connected меняются отдельными функциями. + +## Каналы и участники + +channel_put требует публичные ключи по 32 байта и подпись 64 байта. +При конфликте channel_id публичные ключи должны совпасть: меняются имя/владелец/подпись, +переданные privkey заменяют прежние, NULL сохраняет их. Криптографическую проверку делает вызывающий. + +Суффикс таблиц строится из первых 63 символов channel_id: вне [A-Za-z0-9_] заменяются на '_'. +Штатные channel_id — десятичные uint64. Строка channels.channel_id хранит исходное значение. + +member_placeholder_put вставляет source=1 с нулевой join_sig; существующую строку не меняет. +member_block_put заменяет ключи/join/update/userinfo/дерево приглашений и устанавливает source=0, +сохраняя owner-блок и локальные поля. NULL join_sig записывается как 64 нулевых байта. +member_owner_put меняет adm_tags/подпись/storage/deleted без вставки отсутствующей строки. +Placeholder не участвует в Merkle-хеше; сетевой формат и проверка версий принадлежат member_sync. + +member_exists, member_in_channel, get_member_channels учитывают любое наличие строки, +в том числе source=1 и deleted=1. Это не проверка полной подписанной записи участника. +member_get_join требует ровно 64 байта подписи; channel_get проверяет размер запрошенных ключей/подписи. + +member_del физически удаляет строку без tombstone и пересчёта Merkle. +channel_delete удаляет peers/msg/feedback, канал с приватными ключами и хеши datasets его namespace +(если таблица Merkle существует). Все действия, включая DROP TABLE, защищены savepoint: +ошибка возвращает -1 и откатывает удаление, сохраняя внешнюю транзакцию. + +## Выбор пиров + +Все выборки возвращают принадлежащий вызывающему массив. Пустой результат — NULL/0. +Ошибка чтения/памяти — -1 без публикации частичного массива. + +| API | Выборка | +| --- | --- | +| get_connect_peers | Все неудалённые участники канала, включая self; connected, затем известный RTT/RTT/node_id | +| get_connected_peers | connected=1, deleted=0 в указанном канале | +| get_supernode_peers | node_type=4, deleted=0 в указанном канале | +| get_public_peers | Глобальные адреса addr_type 1/2 с записью nodes, кроме self и удалённых в указанном канале | +| get_local_peers | Аналогично public, но addr_type 0/3 | + +Public/local не требуют членства в указанном канале и не фильтруют port=0. +Для get_connect_peers роль supernode берётся из adm_tags, client_type — из сохранённого +подписанного snapshot; отсутствие снимка даёт UINT8_MAX. Адреса выборку не ограничивают. +Сам запрос не повторяет криптографическую проверку BLOB; подпись проверяется при сохранении снимка. + +Полный список функций, размеры буферов и возвраты — в topo_node_sqlite.h. diff --git a/src/transport_layer/etcp.c b/src/transport_layer/etcp.c index e4325cd7..16f8c011 100644 --- a/src/transport_layer/etcp.c +++ b/src/transport_layer/etcp.c @@ -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); diff --git a/src/transport_layer/etcp.h b/src/transport_layer/etcp.h index 92b28dd0..4d5075ea 100644 --- a/src/transport_layer/etcp.h +++ b/src/transport_layer/etcp.h @@ -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); diff --git a/src/transport_layer/etcp_connections.h b/src/transport_layer/etcp_connections.h index 47b8fa20..6843985b 100644 --- a/src/transport_layer/etcp_connections.h +++ b/src/transport_layer/etcp_connections.h @@ -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 до первого измерения diff --git a/src/transport_layer/etcp_tx.c b/src/transport_layer/etcp_tx.c index b6242ad3..0ee6e65d 100644 --- a/src/transport_layer/etcp_tx.c +++ b/src/transport_layer/etcp_tx.c @@ -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) { diff --git a/src/transport_layer/etcp_tx.h b/src/transport_layer/etcp_tx.h index 169cd7ac..079ebb6e 100644 --- a/src/transport_layer/etcp_tx.h +++ b/src/transport_layer/etcp_tx.h @@ -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 diff --git a/tests/Makefile.am b/tests/Makefile.am index 1a2aef68..4a05f8ae 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -62,6 +62,7 @@ check_PROGRAMS = \ test_merkle_protocol \ test_ncd_config \ test_node_snapshot \ + test_topo_node_wire \ test_route6_lib \ test_etcp_transport \ test_stcp_ping \ @@ -732,6 +733,12 @@ test_node_snapshot_SOURCES = test_node_snapshot.c test_node_snapshot_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_node_snapshot_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +test_topo_node_wire_SOURCES = test_topo_node_wire.c +test_topo_node_wire_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +if OS_LINUX +test_topo_node_wire_LDFLAGS = -Wl,--wrap=u_malloc_impl -Wl,--wrap=u_calloc_impl -Wl,--wrap=memory_pool_alloc_impl +endif + test_ncd_config_SOURCES = test_ncd_config.c test_ncd_config_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_ncd_config_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) @@ -1057,7 +1064,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 diff --git a/tests/test_etcp_multipath.c b/tests/test_etcp_multipath.c index 35c7578a..85d9987a 100644 --- a/tests/test_etcp_multipath.c +++ b/tests/test_etcp_multipath.c @@ -2,9 +2,11 @@ #include #include #include +#include #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; } diff --git a/tests/test_etcp_windows.c b/tests/test_etcp_windows.c index 993df7f8..d7115687 100644 --- a/tests/test_etcp_windows.c +++ b/tests/test_etcp_windows.c @@ -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, diff --git a/tests/test_node_snapshot.c b/tests/test_node_snapshot.c index 54af4728..c6d3abda 100644 --- a/tests/test_node_snapshot.c +++ b/tests/test_node_snapshot.c @@ -4,6 +4,7 @@ #include "utun_instance.h" #include "routing_layer/topo_group.h" #include "routing_layer/topo_node_sqlite.h" +#include "routing_layer/route_lib.h" #include "transport_layer/secure_channel.h" #include "transport_layer/etcp.h" #include "config_parser.h" @@ -63,6 +64,82 @@ static void test_zero_port_schema(void) { sqlite3_close(db); } +/* Прерывает VM SQLite для проверки незавершённого чтения списка. */ +static int interrupt_sql(void* arg) { (void)arg; return 1; } + +/* Отказ SQL не должен выглядеть успешным сохранением или чтением узла. */ +static void test_verified_write(void) { + sqlite3* db = NULL; uint8_t key[32] = {1}; + assert(sqlite3_open(":memory:", &db) == SQLITE_OK && !topo_node_sqlite_init(db)); + assert(!topo_node_sqlite_node_update_verified(db, 7, "before", key, key, 100, 1)); + assert(sqlite3_exec(db, "CREATE TRIGGER reject_node BEFORE INSERT ON nodes " + "BEGIN SELECT RAISE(ABORT,'test insert rejection'); END", NULL, NULL, NULL) == SQLITE_OK); + assert(topo_node_sqlite_node_update_verified(db, 8, "rejected", key, key, 101, 1) < 0); + assert(sqlite3_exec(db, "DROP TRIGGER reject_node; CREATE TRIGGER reject_node BEFORE UPDATE ON nodes " + "BEGIN SELECT RAISE(ABORT,'test update rejection'); END", NULL, NULL, NULL) == SQLITE_OK); + assert(topo_node_sqlite_node_update_verified(db, 7, "rejected", key, key, 101, 1) < 0); + assert(topo_node_sqlite_node_set_online(db, 7, 0) < 0 && topo_node_sqlite_node_get_online(db, 7) == 1); + sqlite3_stmt* st = NULL; + assert(sqlite3_prepare_v2(db, "SELECT name,update_ts FROM nodes ORDER BY node_id", -1, &st, NULL) == SQLITE_OK); + assert(sqlite3_step(st) == SQLITE_ROW && !strcmp((const char*)sqlite3_column_text(st, 0), "before")); + assert(sqlite3_column_int64(st, 1) == 100 && sqlite3_step(st) == SQLITE_DONE); sqlite3_finalize(st); + assert(sqlite3_exec(db, "DROP TRIGGER reject_node", NULL, NULL, NULL) == SQLITE_OK); + assert(!topo_node_sqlite_node_update_verified(db, 7, "after", key, key, 101, 1)); + assert(sqlite3_prepare_v2(db, "SELECT name,update_ts FROM nodes WHERE node_id=7", -1, &st, NULL) == SQLITE_OK); + assert(sqlite3_step(st) == SQLITE_ROW && !strcmp((const char*)sqlite3_column_text(st, 0), "after")); + assert(sqlite3_column_int64(st, 1) == 101); sqlite3_finalize(st); + uint8_t signature[64] = {0}; + assert(!topo_node_sqlite_channel_put(db, "123", "test", 7, key, NULL, key, NULL, signature)); + assert(!topo_node_sqlite_member_placeholder_put(db, "123", 7, key, key)); + assert(!topo_node_sqlite_channel_put(db, "124", "test", 7, key, NULL, key, NULL, signature)); + assert(!topo_node_sqlite_member_placeholder_put(db, "124", 7, key, key)); + assert(!topo_node_sqlite_set_connected(db, "123", 7, 1)); + uint64_t* ids = NULL; int count = 0; + assert(!topo_node_sqlite_get_connected_peers(db, "123", &ids, &count) && count == 1 && ids[0] == 7); u_free(ids); + assert(!topo_node_sqlite_get_member_channels(db, 7, &ids, &count) && count == 2); + assert((ids[0] == 123 && ids[1] == 124) || (ids[0] == 124 && ids[1] == 123)); u_free(ids); + for (int query = 0; query < 5; query++) { + ids = (void*)(uintptr_t)1; count = 99; + sqlite3_progress_handler(db, 1, interrupt_sql, NULL); + int rc = query == 0 ? topo_node_sqlite_get_connected_peers(db, "123", &ids, &count) : + query == 1 ? topo_node_sqlite_get_supernode_peers(db, "123", &ids, &count) : + query == 2 ? topo_node_sqlite_get_public_peers(db, "123", &ids, &count, 0) : + query == 3 ? topo_node_sqlite_get_local_peers(db, "123", &ids, &count, 0) : + topo_node_sqlite_get_member_channels(db, 7, &ids, &count); + sqlite3_progress_handler(db, 0, NULL, NULL); + assert(rc < 0 && !ids && !count && !sqlite3_next_stmt(db, NULL)); + } + assert(sqlite3_exec(db, "UPDATE peers_123 SET node_type=2; UPDATE peers_124 SET node_type=4;" + "CREATE TRIGGER reject_classification BEFORE UPDATE ON peers_124 " + "BEGIN SELECT RAISE(ABORT,'test classification rejection'); END", NULL, NULL, NULL) == SQLITE_OK); + assert(topo_node_sqlite_nodeinfo_updated(db, 7) < 0); + assert(topo_node_sqlite_get_node_type(db, "123", 7) == 2 && topo_node_sqlite_get_node_type(db, "124", 7) == 4); + assert(sqlite3_exec(db, "DROP TRIGGER reject_classification; CREATE TRIGGER reject_channel BEFORE DELETE ON channels " + "BEGIN SELECT RAISE(ABORT,'test channel deletion rejection'); END", NULL, NULL, NULL) == SQLITE_OK); + assert(topo_node_sqlite_channel_delete(db, "124") < 0 && topo_node_sqlite_member_in_channel(db, "124", 7)); + assert(!topo_node_sqlite_channel_get(db, "124", NULL, 0, NULL, NULL, NULL, NULL)); + assert(sqlite3_exec(db, "DROP TRIGGER reject_channel", NULL, NULL, NULL) == SQLITE_OK); + assert(!topo_node_sqlite_nodeinfo_updated(db, 7) && topo_node_sqlite_get_node_type(db, "123", 7) == 0); + assert(!topo_node_sqlite_channel_delete(db, "124")); + assert(topo_node_sqlite_channel_get(db, "124", NULL, 0, NULL, NULL, NULL, NULL) < 0); + assert(sqlite3_exec(db, "CREATE TRIGGER reject_member BEFORE DELETE ON peers_123 " + "BEGIN SELECT RAISE(ABORT,'test member deletion rejection'); END", NULL, NULL, NULL) == SQLITE_OK); + assert(topo_node_sqlite_member_del(db, "123", 7) < 0 && topo_node_sqlite_member_in_channel(db, "123", 7)); + assert(sqlite3_exec(db, "DROP TRIGGER reject_member; UPDATE peers_123 SET join_sig=X'01'; " + "UPDATE channels SET x25519_pubkey=X'01'", NULL, NULL, NULL) == SQLITE_OK); + uint64_t timestamp = 77; memset(signature, 0xa5, sizeof(signature)); + assert(topo_node_sqlite_member_get_join(db, "123", 7, signature, ×tamp) < 0); + assert(timestamp == 77 && signature[0] == 0xa5); + memset(key, 0xa5, sizeof(key)); + assert(topo_node_sqlite_channel_get(db, "123", NULL, 0, NULL, key, NULL, NULL) < 0 && key[0] == 0xa5); + assert(!topo_node_sqlite_member_del(db, "123", 7) && !topo_node_sqlite_member_in_channel(db, "123", 7)); + assert(!topo_node_sqlite_node_set_online(db, 7, 0) && !topo_node_sqlite_node_get_online(db, 7)); + assert(sqlite3_exec(db, "DROP TABLE nodes", NULL, NULL, NULL) == SQLITE_OK); + assert(topo_node_sqlite_node_update_verified(db, 7, "no table", key, key, 102, 1) < 0); + sqlite3_close(db); + DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] verified node writes: insert/update, rejected writes, prepare failure and stored state"); +} + /* Новый wire и SQL сохраняют адреса, протоколы, классификацию и REALITY; смена формата оставляет bootstrap. */ static void test_compact_addresses(struct UTUN_INSTANCE *inst) { test_zero_port_schema(); @@ -142,11 +219,47 @@ static void test_compact_addresses(struct UTUN_INSTANCE *inst) { sqlite3_close(db); inst->topo_sqlite_db = saved_db; } +/* Каждый размер буфера: длина, NUL, защитные байты, сохранённый префикс и маркер усечения. */ +static void test_node_format(struct TOPO_GROUP* group) { + char full[8192]; + int full_len = topo_node_format_all(group, full, sizeof(full)); + assert(full_len > 0 && (size_t)full_len == strlen(full) && !strstr(full, "[TRUNCATED]")); + assert(strstr(full, "=== BGP Nodes dump:")); + if (group->nodes->head) assert(strstr(full, " v4addr[0]:") && strstr(full, " path[0]:")); + const char marker[] = "\n[TRUNCATED]\n"; + debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_ERROR); + for (size_t capacity = 0; capacity <= (size_t)full_len + 2; capacity++) { + uint8_t* storage = u_malloc(capacity + 2); + assert(storage); memset(storage, 0xa5, capacity + 2); + char* buf = (char*)storage + 1; + int length = topo_node_format_all(group, buf, capacity); + assert(storage[0] == 0xa5 && storage[capacity + 1] == 0xa5); + if (!capacity) { assert(!length); u_free(storage); continue; } + size_t expected = (size_t)full_len < capacity ? (size_t)full_len : capacity - 1; + assert(length >= 0 && (size_t)length == expected); + assert(memchr(buf, '\0', capacity) == buf + length); + if (capacity > (size_t)full_len) assert(!memcmp(buf, full, full_len + 1)); + else { + size_t marker_len = expected < sizeof(marker) - 1 ? expected : sizeof(marker) - 1; + assert(!memcmp(buf, full, expected - marker_len)); + assert(!memcmp(buf + expected - marker_len, marker, marker_len)); + } + u_free(storage); + } + char empty[] = "old"; + assert(!topo_node_format_all(NULL, empty, sizeof(empty)) && !empty[0]); + assert(!topo_node_format_all(group, NULL, sizeof(empty))); + debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG); + DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] node dump: nodes=%d capacities=0..%d, lengths, guards, NUL and truncation", + queue_entry_count(group->nodes), full_len + 2); +} + static void test_bgp(struct UTUN_INSTANCE* signer) { struct UTUN_INSTANCE receiver = { .ua = signer->ua, .topo_groups = signer->topo_groups }; struct TOPO_GROUP group = { .instance = &receiver, .group_id = 10, .group_type = TOPO_GROUP_TYPE_CHAT }; struct ETCP_CONN from = { .instance = &receiver, .peer_node_id = 7 }; group.nodes = queue_new(receiver.ua, 16, 0, 8, "snapshot_bgp"); + test_node_format(&group); struct TOPO_NODE* ni = record(signer, INT64_MAX - 7, "BGP", 1); from.peer_node_id = ni->node_id; from.links_up = 1; struct TOPO_GROUP_NODE nq = {0}; @@ -158,7 +271,13 @@ static void test_bgp(struct UTUN_INSTANCE* signer) { assert(peer && peer->last_timestamp == ni->timestamp && queue_entry_count(peer->paths) == 1); assert(topo_group_process_nodeinfo(&group, &from, packet, (size_t)len + sizeof(struct TOPOMSG_HEADER)) == 0); assert(queue_entry_count(peer->paths) == 1); + test_node_format(&group); struct TOPOMSG_NODEINFO_PKT* wire = (struct TOPOMSG_NODEINFO_PKT*)packet; + wire->node.local_v4_subnets = 1; /* malformed CHAT не меняет уже принятую запись и маршрут */ + assert(topo_group_process_nodeinfo(&group, &from, packet, (size_t)len + sizeof(struct TOPOMSG_HEADER)) < 0); + assert(topo_node_find_by_id(&group, ni->node_id) == peer && queue_entry_count(peer->paths) == 1); + assert(peer->last_timestamp == ni->timestamp && topo_node_registry_find(signer->topo_groups, ni->node_id)->timestamp == ni->timestamp); + wire->node.local_v4_subnets = 0; wire->node.timestamp++; /* подделка не удаляет узел и ранее принятый маршрут */ assert(topo_group_process_nodeinfo(&group, &from, packet, (size_t)len + sizeof(struct TOPOMSG_HEADER)) < 0); assert(topo_node_find_by_id(&group, ni->node_id) == peer && queue_entry_count(peer->paths) == 1); @@ -174,6 +293,44 @@ static void test_bgp(struct UTUN_INSTANCE* signer) { queue_free(group.nodes); } +/* Замена подсети при том же количестве и откат ошибок сохраняют согласованные local_node/route table. */ +static void test_local_group_update(struct UTUN_INSTANCE* inst) { + sqlite3* saved_db = inst->topo_sqlite_db; + assert(sqlite3_open(":memory:", &inst->topo_sqlite_db) == SQLITE_OK && !topo_node_sqlite_init(inst->topo_sqlite_db)); + struct CFG_ROUTE_ENTRY subnet = {.ip.family = AF_INET, .netmask = 24}; + subnet.ip.addr.v4.s_addr = htonl(0x0a010000); + struct utun_config config = {.my_subnets = &subnet}; + inst->config = &config; inst->rt = route_table_create(inst->node_id); assert(inst->rt); + struct TOPO_GROUP group = {.instance = inst, .group_type = TOPO_GROUP_TYPE_UTUN, .group_id = TOPO_GROUP_UTUN}; + memcpy(group.ed25519_public_key, inst->my_ed25519_pubkey, 32); + assert(topo_group_update_my_nodeinfo(inst, &group) == 1 && route_lookup(inst->rt, 0x0a010001)); + subnet.ip.addr.v4.s_addr = htonl(0x0a020000); + assert(topo_group_update_my_nodeinfo(inst, &group) == 1 && route_lookup(inst->rt, 0x0a020001)); + assert(!route_lookup(inst->rt, 0x0a010001)); + struct TOPO_GROUP_NODE* before = group.local_node; + struct TOPO_NODE* identity = topo_node_registry_find(inst->topo_groups, inst->node_id); + uint64_t timestamp = identity->timestamp; uint32_t refs = identity->group_ref_count; + assert(sqlite3_exec(inst->topo_sqlite_db, "CREATE TRIGGER reject_local BEFORE UPDATE ON node_snapshots " + "BEGIN SELECT RAISE(ABORT,'test local identity rejection'); END", NULL, NULL, NULL) == SQLITE_OK); + subnet.ip.addr.v4.s_addr = htonl(0x0a030000); + assert(topo_group_update_my_nodeinfo(inst, &group) < 0 && group.local_node == before); + assert(route_lookup(inst->rt, 0x0a020001) && !route_lookup(inst->rt, 0x0a030001)); + assert(identity->timestamp == timestamp && identity->group_ref_count == refs && !topo_node_verify(identity)); + assert(sqlite3_exec(inst->topo_sqlite_db, "DROP TRIGGER reject_local", NULL, NULL, NULL) == SQLITE_OK); + struct TOPO_SUBNET4 remote_sub = {.addr = {10, 3, 0, 0}, .prefix_length = 24}; + struct TOPO_NODESUBNETS remote_subnets = {.v4_subnets = &remote_sub}; + struct TOPO_GROUP_NODE remote = {.node_id = 999, .subnets = &remote_subnets}; + assert(route_insert(inst->rt, &remote)); + assert(topo_group_update_my_nodeinfo(inst, &group) < 0 && group.local_node == before); + assert(route_lookup(inst->rt, 0x0a020001)->v_node_info == before && route_lookup(inst->rt, 0x0a030001)->v_node_info == &remote); + assert(identity->timestamp == timestamp && identity->group_ref_count == refs && !topo_node_verify(identity)); + route_table_destroy(inst->rt); inst->rt = NULL; + topo_nodeq_free_group_fields(inst->topo_groups, group.local_node); u_free(group.local_node); + assert(!inst->topo_groups->node_registry->head); + sqlite3_close(inst->topo_sqlite_db); inst->topo_sqlite_db = saved_db; inst->config = NULL; + DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] local group: same-count subnet replacement, signature failure and route conflict rollback"); +} + int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_DEBUG); @@ -187,6 +344,8 @@ int main(void) { groups.v4_sock_meta_pool = memory_pool_init(sizeof(struct TOPO_SOCKMETA4), "snapshot_meta4"); groups.v6_sock_meta_pool = memory_pool_init(sizeof(struct TOPO_SOCKMETA6), "snapshot_meta6"); groups.v6_addr_pool = memory_pool_init(sizeof(struct TOPO_ADDR6), "snapshot_v6"); + groups.v4_subnet_pool = memory_pool_init(sizeof(struct TOPO_SUBNET4), "snapshot_subnet4"); + groups.v6_subnet_pool = memory_pool_init(sizeof(struct TOPO_SUBNET6), "snapshot_subnet6"); assert(groups.v4_addr_pool && groups.v4_sock_meta_pool && groups.v6_sock_meta_pool && groups.v6_addr_pool); assert(sc_generate_keypair(&inst.my_keys) == SC_OK); memcpy(inst.my_ed25519_privkey, inst.my_keys.private_key, 32); @@ -197,7 +356,9 @@ int main(void) { inst.node_id = sc_derive_node_id_from_pubkey(inst.my_keys.public_key); assert(sqlite3_open(":memory:", &inst.topo_sqlite_db) == SQLITE_OK); assert(topo_node_sqlite_init(inst.topo_sqlite_db) == 0); + test_verified_write(); test_compact_addresses(&inst); + test_local_group_update(&inst); struct TOPO_NODE* first = record(&inst, 100, "old", 1); assert(topo_node_verify(first) == 0); @@ -260,6 +421,8 @@ int main(void) { memory_pool_destroy(groups.v4_sock_meta_pool); memory_pool_destroy(groups.v6_sock_meta_pool); memory_pool_destroy(groups.v6_addr_pool); + memory_pool_destroy(groups.v4_subnet_pool); + memory_pool_destroy(groups.v6_subnet_pool); sqlite3_close(inst.topo_sqlite_db); uasync_destroy(inst.ua, 0); return 0; diff --git a/tests/test_reality_bgp.c b/tests/test_reality_bgp.c index 7bd478ed..7b4ff399 100644 --- a/tests/test_reality_bgp.c +++ b/tests/test_reality_bgp.c @@ -8,7 +8,9 @@ #include "topo_node.h" #include "topo_group.h" #include "etcp_connections.h" +#include "utun_instance.h" #include "../lib/debug_config.h" +#include "../lib/mem.h" #include #include @@ -67,7 +69,10 @@ int main(void) { uint8_t buf[2048]; int len = topo_node_serialize(&ni, &nq, 0x1234, 0, buf, sizeof(buf), 0); CHECK(len > 0, "serialize ok"); - struct TOPO_GROUP g; memset(&g, 0, sizeof(g)); + struct UTUN_INSTANCE instance = {0}; + struct TOPO_GROUPS groups = {.instance = &instance}; + instance.topo_groups = &groups; + struct TOPO_GROUP g = {.instance = &instance}; struct TOPO_NODE* out_ni = NULL; struct TOPO_NODESUBNETS* out_sub = NULL; uint64_t* hop = NULL; uint8_t hopc = 0; uint16_t rtt = 0; @@ -87,6 +92,9 @@ int main(void) { CHECK(rr1 != NULL && !strcmp(rr1->server_name, long_sni), "round-trip: full server_name"); CHECK(rr1 != NULL && rr1->version[0] == 1 && rr1->version[1] == 2 && rr1->version[2] == 3, "round-trip: version"); } + CHECK(!out_sub && !hop && !hopc && !rtt, "round-trip: empty optional sections"); + topo_node_destroy(&groups, out_ni); + u_free(hop); if (test_failed) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "=== Reality BGP Test: FAILED ==="); diff --git a/tests/test_route_lib.c b/tests/test_route_lib.c index 812922ce..9e4f4b7b 100644 --- a/tests/test_route_lib.c +++ b/tests/test_route_lib.c @@ -1,16 +1,4 @@ -/** - * FULL REFACTORED TEST for new NODEINFO-based routing system - * - * Tests: - * - TOPO_GROUP_NODE creation and insertion - * - paths management (add/remove) - * - versioning (ver field) - * - WITHDRAW when no paths left - * - Performance with 1000 nodes - * - Overlap prevention and lookup - * - * Status: Complete coverage of current route_lib + route_bgp - */ +/* IPv4-таблица: владение, непересекающиеся подсети, атомарный отказ и поиск на границах. */ #include @@ -44,13 +32,8 @@ struct UTUN_INSTANCE { static struct TOPO_GROUP_NODE* create_test_node(uint64_t node_id, uint8_t ver, uint32_t subnet_net, uint8_t prefix) { struct TOPO_GROUP_NODE* nq = u_calloc(1, sizeof(struct TOPO_GROUP_NODE)); if (!nq) return NULL; - struct TOPO_NODE* ni = u_calloc(1, sizeof(struct TOPO_NODE)); - if (!ni) { u_free(nq); return NULL; } - ni->group_ref_count = 1; - ni->node_id = htobe64(node_id); - ni->ver = ver; - nq->node_id = ni->node_id; - nq->paths = queue_new(NULL, 16, 0, 0, "test_paths"); + (void)ver; + nq->node_id = node_id; struct TOPO_NODESUBNETS* r = u_calloc(1, sizeof(struct TOPO_NODESUBNETS)); if (r) { struct TOPO_SUBNET4* sn = u_calloc(1, sizeof(struct TOPO_SUBNET4)); @@ -152,11 +135,12 @@ static void test_performance_1000_nodes(void) { struct ROUTE_TABLE *t = route_table_create(0); clock_t start = clock(); + struct TOPO_GROUP_NODE *nodes[1000]; for (int i = 0; i < 1000; i++) { uint32_t sub = 0x0a000000 + (i * 0x100); /* distinct 10.0.x.0/24 to avoid overlap */ struct TOPO_GROUP_NODE* nq = create_test_node(0x1000000ULL + i, 1, sub, 24); + nodes[i] = nq; route_insert(t, nq); - /* note: nq not freed here (table holds ref to it), freed in test cleanup if needed */ } clock_t end = clock(); @@ -166,6 +150,87 @@ static void test_performance_1000_nodes(void) { printf("(%.3fs for 1000 nodes) ", time); route_table_destroy(t); + for (int i = 0; i < 1000; i++) free_test_node(nodes[i]); + PASS(); +} + +/* Проверяет оба порядка вложенных подсетей, дубликаты, host-биты и соседние диапазоны. */ +static void test_overlap_and_lookup(void) { + TEST("overlap rejection and disjoint lookup"); + struct { uint32_t first, second; uint8_t p1, p2; bool accepted; } cases[] = { + {0x0a000000, 0x0a010000, 8, 24, false}, + {0x0a010000, 0x0a000000, 24, 8, false}, + {0x0a010001, 0x0a0100ff, 24, 24, false}, + {0x0a010001, 0x0a010001, 32, 32, false}, + {0x0a010000, 0x0a010002, 31, 31, true}, + {0x0a010001, 0x0a010002, 32, 32, true}, + {0, 0x0a000001, 0, 32, false}, + {0x0a000001, 0, 32, 0, false} + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + struct ROUTE_TABLE *t = route_table_create(1); + struct TOPO_GROUP_NODE *a = create_test_node(1, 0, cases[i].first, cases[i].p1); + struct TOPO_GROUP_NODE *b = create_test_node(2, 0, cases[i].second, cases[i].p2); + ASSERT(route_insert(t, a), "first insertion"); + ASSERT_EQ(route_insert(t, b), cases[i].accepted, "overlap decision"); + ASSERT_EQ(t->count, cases[i].accepted ? 2 : 1, "atomic table count"); + ASSERT_EQ(t->stats.local_routes, 1, "local route count"); + ASSERT_EQ(t->stats.learned_routes, cases[i].accepted ? 1 : 0, "learned route count"); + struct ROUTE_ENTRY *found = route_lookup(t, cases[i].first); + ASSERT(found && found->v_node_info == a, "first route lookup"); + if (cases[i].accepted) { + found = route_lookup(t, cases[i].second); + ASSERT(found && found->v_node_info == b, "second route lookup"); + } + route_delete(t, a); + route_delete(t, b); + ASSERT_EQ(t->count, 0, "delete clears routes"); + route_table_destroy(t); free_test_node(a); free_test_node(b); + } + PASS(); +} + +/* Ни поздний конфликт набора, ни неверный префикс не должны оставлять частичную вставку. */ +static void test_batch_atomicity(void) { + TEST("batch rejection leaves table unchanged"); + struct ROUTE_TABLE *t = route_table_create(1); + struct TOPO_GROUP_NODE *a = create_test_node(1, 0, 0x0a000000, 8); + struct TOPO_GROUP_NODE *b = create_test_node(2, 0, 0xc0a80000, 24); + ASSERT(route_insert(t, a), "base route"); + struct TOPO_SUBNET4 second = {0}; + uint32_t network = htonl(0x0a010000); + memcpy(second.addr, &network, 4); second.prefix_length = 24; + b->subnets->v4_subnets->next = &second; + ASSERT(!route_insert(t, b), "late overlap with table"); + ASSERT_EQ(t->count, 1, "no partial insert before table conflict"); + network = htonl(0xc0a80080); memcpy(second.addr, &network, 4); + ASSERT(!route_insert(t, b), "overlap inside batch"); + ASSERT_EQ(t->count, 1, "no partial insert before batch conflict"); + second.prefix_length = 33; + ASSERT(!route_insert(t, b), "invalid prefix rejected"); + ASSERT_EQ(t->count, 1, "invalid prefix preserves table"); + ASSERT_EQ(t->stats.learned_routes, 0, "rejection preserves statistics"); + b->subnets->v4_subnets->next = NULL; + route_table_destroy(t); free_test_node(a); free_test_node(b); + PASS(); +} + +static void test_lookup_boundaries(void) { + TEST("normalized subnet boundaries and default route"); + struct ROUTE_TABLE *t = route_table_create(1); + struct TOPO_GROUP_NODE *a = create_test_node(1, 0, 0x0a00007b, 24); + ASSERT(route_insert(t, a), "noncanonical address accepted"); + ASSERT_EQ(t->entries[0].network, 0x0a000000, "network normalized"); + ASSERT(route_lookup(t, 0x0a000000), "first address"); + ASSERT(route_lookup(t, 0x0a0000ff), "last address"); + ASSERT(!route_lookup(t, 0x09ffffff), "before range"); + ASSERT(!route_lookup(t, 0x0a000100), "after range"); + route_delete(t, a); + a->subnets->v4_subnets->prefix_length = 0; + ASSERT(route_insert(t, a), "default route"); + ASSERT(route_lookup(t, 0), "default first address"); + ASSERT(route_lookup(t, UINT32_MAX), "default last address"); + route_table_destroy(t); free_test_node(a); PASS(); } @@ -187,6 +252,9 @@ int main(void) { test_table_create_destroy(); test_nodeinfo_insert(); test_versioning(); + test_overlap_and_lookup(); + test_batch_atomicity(); + test_lookup_boundaries(); test_performance_1000_nodes(); test_destroy_refcount(); diff --git a/tests/test_topo_node_wire.c b/tests/test_topo_node_wire.c new file mode 100644 index 00000000..a3663684 --- /dev/null +++ b/tests/test_topo_node_wire.c @@ -0,0 +1,368 @@ +/* Ручной NODEINFO: границы секций, необязательные выходы и атомарная очистка при отказах памяти. */ +#include +#include +#include +#include +#include "utun_instance.h" +#include "routing_layer/topo_group.h" +#include "routing_layer/topo_node_sqlite.h" +#include "routing_layer/route_lib.h" +#include "config_parser.h" +#include "transport_layer/etcp.h" +#include "../lib/debug_config.h" +#include "../lib/mem.h" +#include "../lib/memory_pool.h" +#include "../lib/u_async.h" + +struct wire_fixture { + struct UTUN_INSTANCE inst; + struct TOPO_GROUPS groups; + struct TOPO_GROUP group; + struct memory_pool *pools[6]; + uint8_t wire[2048]; + size_t length, subnet4_offset, subnet6_offset, hops_offset; + uint64_t hops[2]; +}; + +#ifdef __linux__ +static unsigned fail_at, attempts, inside_pool; +static int track_allocations; + +static int allocation_fails(const char *where) { + if (!track_allocations || (!strstr(where, "topo_node.c:") && !strstr(where, "topo_node_sqlite.c:"))) return 0; + attempts++; + if (attempts != fail_at) return 0; + DEBUG_WARN(DEBUG_CATEGORY_BGP, "[fault] NODEINFO allocation %u rejected at %s", attempts, where); + return 1; +} + +void *__real_u_malloc_impl(uint32_t, const char *); +void *__wrap_u_malloc_impl(uint32_t size, const char *where) { + if (!inside_pool && allocation_fails(where)) return NULL; + return __real_u_malloc_impl(size, where); +} + +void *__real_u_calloc_impl(uint32_t, uint32_t, const char *); +void *__wrap_u_calloc_impl(uint32_t count, uint32_t size, const char *where) { + if (!inside_pool && allocation_fails(where)) return NULL; + return __real_u_calloc_impl(count, size, where); +} + +void *__real_memory_pool_alloc_impl(struct memory_pool *, const char *); +void *__wrap_memory_pool_alloc_impl(struct memory_pool *pool, const char *where) { + if (allocation_fails(where)) return NULL; + inside_pool++; + void *result = __real_memory_pool_alloc_impl(pool, where); + inside_pool--; + return result; +} +#endif + +static void fixture_init(struct wire_fixture *f) { + memset(f, 0, sizeof(*f)); + f->inst.topo_groups = &f->groups; f->groups.instance = &f->inst; f->group.instance = &f->inst; + size_t sizes[] = {sizeof(struct TOPO_SOCKMETA4), sizeof(struct TOPO_ADDR4), sizeof(struct TOPO_SOCKMETA6), + sizeof(struct TOPO_ADDR6), sizeof(struct TOPO_SUBNET4), sizeof(struct TOPO_SUBNET6)}; + for (size_t i = 0; i < 6; i++) { f->pools[i] = memory_pool_init(sizes[i], "node_wire"); assert(f->pools[i]); } + f->groups.v4_sock_meta_pool = f->pools[0]; f->groups.v4_addr_pool = f->pools[1]; + f->groups.v6_sock_meta_pool = f->pools[2]; f->groups.v6_addr_pool = f->pools[3]; + f->groups.v4_subnet_pool = f->pools[4]; f->groups.v6_subnet_pool = f->pools[5]; + f->hops[0] = 0x1111111111111111ULL; f->hops[1] = 0x2222222222222222ULL; +} + +static void fixture_destroy(struct wire_fixture *f) { + for (size_t i = 0; i < 6; i++) memory_pool_destroy(f->pools[i]); +} + +/* Пакет собирается независимо от production encoder; по два элемента проверяют очистку частичных списков. */ +static void fixture_packet(struct wire_fixture *f, uint8_t flags, uint8_t hops) { + memset(f->wire, 0, sizeof(f->wire)); + struct TOPOMSG_NODE *h = (struct TOPOMSG_NODE *)f->wire; + h->node_id = 9; h->flags = flags; h->node_name_len = 4; h->cumulative_rtt = 123; + h->local_v4_sockets = h->local_v4_addrs = h->local_v6_sockets = h->local_v6_addrs = h->local_reality_socks = 2; + if (flags & TOPO_FLAG_SEND_SUBNETS) h->local_v4_subnets = h->local_v6_subnets = 2; + h->hop_count = hops; + uint8_t *p = f->wire + TOPOMSG_NODE_HDR_SIZE; + memcpy(p, "wire", 4); p += 4; + for (unsigned i = 0; i < 2; i++) { + struct TOPOMSG_SOCKMETA4 m = {.id = i}; memcpy(p, &m, sizeof(m)); p += sizeof(m); + } + for (unsigned i = 0; i < 2; i++) { + struct TOPOMSG_ADDR4 a = {.addr = {10, 0, 0, i + 1}, .port = 9000 + i, .socket_id = i, .protocol = TOPO_PROTO_UDP}; + memcpy(p, &a, sizeof(a)); p += sizeof(a); + } + for (unsigned i = 0; i < 2; i++) { + struct TOPOMSG_SOCKMETA6 m = {.id = i}; memcpy(p, &m, sizeof(m)); p += sizeof(m); + } + for (unsigned i = 0; i < 2; i++) { + struct TOPOMSG_ADDR6 a = {.addr = {0x20, 1, 0xdb, 8}, .port = 9100 + i, .socket_id = i, .protocol = TOPO_PROTO_TCP}; + a.addr[15] = i + 1; memcpy(p, &a, sizeof(a)); p += sizeof(a); + } + for (unsigned i = 0; i < 2; i++) { + struct TOPOMSG_REALITY_SOCK r = {.socket_id = i}; + memcpy(r.server_name, i ? "second.test" : "first.test", i ? 12 : 11); + memcpy(p, &r, sizeof(r)); p += sizeof(r); + } + f->subnet4_offset = p - f->wire; + if (flags & TOPO_FLAG_SEND_SUBNETS) for (unsigned i = 0; i < 2; i++) { + struct TOPOMSG_SUBNET4 s = {.addr = {10, i, 0, 0}, .prefix_length = 24}; + memcpy(p, &s, sizeof(s)); p += sizeof(s); + } + f->subnet6_offset = p - f->wire; + if (flags & TOPO_FLAG_SEND_SUBNETS) for (unsigned i = 0; i < 2; i++) { + struct TOPOMSG_SUBNET6 s = {.addr = {0x20, 1, 0xdb, 8}, .prefix_length = 64}; + s.addr[7] = i; memcpy(p, &s, sizeof(s)); p += sizeof(s); + } + f->hops_offset = p - f->wire; + memcpy(p, f->hops, hops * sizeof(*f->hops)); p += hops * sizeof(*f->hops); + f->length = p - f->wire; + assert(f->length <= sizeof(f->wire)); +} + +/* Свободные блоки остаются в кешах пулов; сравниваем только реально удерживаемые объекты. */ +static size_t live_allocations(void) { + return u_get_allocated_count() - memory_pool_get_total_free_blocks(); +} + +static void result_free(struct wire_fixture *f, struct TOPO_NODE *node, struct TOPO_NODESUBNETS *subnets, uint64_t *hops) { + topo_node_destroy(&f->groups, node); + struct TOPO_GROUP_NODE temporary = {.subnets = subnets}; + topo_nodeq_free_group_fields(&f->groups, &temporary); + u_free(hops); +} + +static void expect_rejected(struct wire_fixture *f, size_t length, int keep_subnets) { + struct TOPO_NODE *node = (void *)(uintptr_t)1; + struct TOPO_NODESUBNETS *subnets = (void *)(uintptr_t)1; + uint64_t *hops = (void *)(uintptr_t)1; + uint8_t count = 99; uint16_t rtt = 99; + size_t before = live_allocations(); + assert(topo_node_deserialize(&f->group, f->wire, length, &node, keep_subnets ? &subnets : NULL, &hops, &count, &rtt) < 0); + assert(!node && !hops && !count && !rtt); + if (keep_subnets) assert(!subnets); + assert(live_allocations() == before); +} + +static void test_invalid_packets(struct wire_fixture *f) { + fixture_packet(f, 0, 2); + struct TOPOMSG_NODE *h = (struct TOPOMSG_NODE *)f->wire; + h->local_v4_subnets = 1; + expect_rejected(f, f->length, 1); + assert(topo_node_dyn_size(h) < 0); + h->local_v4_subnets = 0; h->local_v6_subnets = 1; + expect_rejected(f, f->length, 0); + assert(topo_node_dyn_size(h) < 0); + fixture_packet(f, TOPO_FLAG_SEND_SUBNETS, 2); + for (size_t length = 0; length < f->length; length++) expect_rejected(f, length, length % 2); + expect_rejected(f, f->length + 1, 1); + for (int keep = 0; keep < 2; keep++) { + struct TOPOMSG_SUBNET4 *v4 = (struct TOPOMSG_SUBNET4 *)(f->wire + f->subnet4_offset); + v4->prefix_length = 33; expect_rejected(f, f->length, keep); v4->prefix_length = 24; + struct TOPOMSG_SUBNET6 *v6 = (struct TOPOMSG_SUBNET6 *)(f->wire + f->subnet6_offset); + v6->prefix_length = 129; expect_rejected(f, f->length, keep); v6->prefix_length = 64; + } + struct TOPOMSG_SOCKMETA4 *meta = (struct TOPOMSG_SOCKMETA4 *)(f->wire + TOPOMSG_NODE_HDR_SIZE + 4); + meta[1].mapping = UINT8_MAX; expect_rejected(f, f->length, 1); + DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] NODEINFO malformed counters, all truncations, tail, prefixes and partial metadata"); +} + +static void test_optional_outputs(struct wire_fixture *f) { + for (unsigned with_subnets = 0; with_subnets < 2; with_subnets++) for (unsigned with_hops = 0; with_hops < 2; with_hops++) { + fixture_packet(f, with_subnets ? TOPO_FLAG_SEND_SUBNETS : 0, with_hops ? 2 : 0); + for (unsigned outputs = 0; outputs < 16; outputs++) { + struct TOPO_NODE *node = NULL; struct TOPO_NODESUBNETS *subnets = NULL; + uint64_t *hops = NULL; uint8_t count = 99; uint16_t rtt = 99; + size_t before = live_allocations(); + assert(!topo_node_deserialize(&f->group, f->wire, f->length, &node, outputs & 1 ? &subnets : NULL, + outputs & 2 ? &hops : NULL, outputs & 4 ? &count : NULL, outputs & 8 ? &rtt : NULL)); + assert(node && !strcmp(node->node_name, "wire")); + assert(node->v4_addrs->addr[3] == 1 && node->v4_addrs->next->addr[3] == 2); + assert(node->v6_addrs->addr[15] == 1 && node->v6_addrs->next->addr[15] == 2); + assert(!strcmp(node->reality_socks->server_name, "first.test")); + assert(!strcmp(node->reality_socks->next->server_name, "second.test")); + if (outputs & 1) { + assert(!!subnets == !!with_subnets); + if (subnets) assert(topo_list_count((struct _topo_head *)subnets->v4_subnets) == 2 && + topo_list_count((struct _topo_head *)subnets->v6_subnets) == 2); + } + if (outputs & 2) { + assert(!!hops == !!with_hops); + if (hops) assert(!memcmp(hops, f->hops, sizeof(f->hops))); + } + if (outputs & 4) assert(count == (with_hops ? 2 : 0)); + if (outputs & 8) assert(rtt == 123); + result_free(f, node, subnets, hops); + assert(live_allocations() == before); + } + } + DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] NODEINFO 64 flag/hop/output combinations preserve identity, hops and ownership"); +} + +static void test_encoder(struct wire_fixture *f) { + fixture_packet(f, TOPO_FLAG_SEND_SUBNETS, 0); + struct TOPO_NODE *node = NULL; struct TOPO_NODESUBNETS *subnets = NULL; + assert(!topo_node_deserialize(&f->group, f->wire, f->length, &node, &subnets, NULL, NULL, NULL)); + struct TOPO_GROUP_NODE nq = {.subnets = subnets}; + uint8_t encoded[2048]; + for (unsigned with_subnets = 0; with_subnets < 2; with_subnets++) { + uint8_t flags = with_subnets ? TOPO_FLAG_SEND_SUBNETS : 0; + int length = topo_node_serialize(node, &nq, 42, flags, encoded, sizeof(encoded), 123); + assert(length > 0); + const struct TOPOMSG_NODE *h = (const struct TOPOMSG_NODE *)encoded; + assert(h->local_v4_subnets == (with_subnets ? 2 : 0) && h->local_v6_subnets == (with_subnets ? 2 : 0)); + assert((size_t)length == TOPOMSG_NODE_HDR_SIZE + (size_t)topo_node_dyn_size(h)); + assert(topo_node_serialize(node, &nq, 42, flags, encoded, length - 1, 123) < 0); + assert(topo_node_serialize(node, &nq, 42, flags, encoded, length, 123) == length); + struct TOPO_NODE *copy = NULL; + assert(!topo_node_deserialize(&f->group, encoded, length, ©, NULL, NULL, NULL, NULL)); + topo_node_destroy(&f->groups, copy); + } + result_free(f, node, subnets, NULL); + DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] NODEINFO encoder emits only selected sections and checks exact buffer capacity"); +} + +static void test_empty_and_arguments(struct wire_fixture *f) { + memset(f->wire, 0, sizeof(f->wire)); + struct TOPOMSG_NODE *h = (struct TOPOMSG_NODE *)f->wire; + for (unsigned with_flag = 0; with_flag < 2; with_flag++) { + h->flags = with_flag ? TOPO_FLAG_SEND_SUBNETS : 0; + struct TOPO_NODE *node = NULL; struct TOPO_NODESUBNETS *subnets = (void *)(uintptr_t)1; + uint64_t *hops = (void *)(uintptr_t)1; uint8_t count = 99; uint16_t rtt = 99; + assert(!topo_node_deserialize(&f->group, f->wire, TOPOMSG_NODE_HDR_SIZE, &node, &subnets, &hops, &count, &rtt)); + assert(node && !node->node_name && !node->v4_addrs && !node->v6_addrs && !subnets && !hops && !count && !rtt); + result_free(f, node, subnets, hops); + } + struct TOPO_NODE *node = (void *)(uintptr_t)1; + assert(topo_node_deserialize(NULL, f->wire, TOPOMSG_NODE_HDR_SIZE, &node, NULL, NULL, NULL, NULL) < 0 && !node); + assert(topo_node_deserialize(&f->group, NULL, TOPOMSG_NODE_HDR_SIZE, &node, NULL, NULL, NULL, NULL) < 0 && !node); + assert(topo_node_deserialize(&f->group, f->wire, TOPOMSG_NODE_HDR_SIZE, NULL, NULL, NULL, NULL, NULL) < 0); + assert(topo_node_dyn_size(NULL) < 0); + fixture_packet(f, TOPO_FLAG_SEND_SUBNETS, 0); + struct TOPOMSG_SUBNET4 *v4 = (struct TOPOMSG_SUBNET4 *)(f->wire + f->subnet4_offset); + struct TOPOMSG_SUBNET6 *v6 = (struct TOPOMSG_SUBNET6 *)(f->wire + f->subnet6_offset); + v4[0].prefix_length = 0; v4[1].prefix_length = 32; + v6[0].prefix_length = 0; v6[1].prefix_length = 128; + assert(!topo_node_deserialize(&f->group, f->wire, f->length, &node, NULL, NULL, NULL, NULL)); + topo_node_destroy(&f->groups, node); + DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] NODEINFO empty sections, missing arguments and prefix boundaries"); +} + +#ifdef __linux__ +static void test_allocation_failures(struct wire_fixture *f) { + fixture_packet(f, TOPO_FLAG_SEND_SUBNETS, 2); + struct TOPO_NODE *node = NULL; struct TOPO_NODESUBNETS *subnets = NULL; + uint64_t *hops = NULL; uint8_t count = 0; + attempts = 0; fail_at = 0; track_allocations = 1; + int rc = topo_node_deserialize(&f->group, f->wire, f->length, &node, &subnets, &hops, &count, NULL); + track_allocations = 0; + assert(!rc && attempts > 10); + unsigned total = attempts; + result_free(f, node, subnets, hops); + for (unsigned nth = 1; nth <= total; nth++) { + attempts = 0; fail_at = nth; track_allocations = 1; + expect_rejected(f, f->length, 1); + track_allocations = 0; + assert(attempts == nth); + } + fail_at = 0; + DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] NODEINFO all %u allocation failures leave empty outputs and no live allocations", total); +} + +/* SQL bootstrap обязан освободить уже прочитанные адреса/Reality при отказе любого следующего выделения. */ +static void test_bootstrap_allocation_failures(struct wire_fixture *f) { + fixture_packet(f, 0, 0); + struct TOPO_NODE *node = NULL; + assert(!topo_node_deserialize(&f->group, f->wire, f->length, &node, NULL, NULL, NULL, NULL)); + node->v4_addrs->protocol = node->v6_addrs->protocol = TOPO_PROTO_TCP | TOPO_PROTO_REALITY; + sqlite3 *db = NULL; + assert(sqlite3_open(":memory:", &db) == SQLITE_OK && !topo_node_sqlite_init(db)); + assert(!topo_node_sqlite_node_update_verified(db, node->node_id, "bootstrap", node->public_key, node->ed25519_public_key, 1, 1)); + assert(!topo_node_sqlite_addrs_put(db, node->node_id, node)); + attempts = 0; fail_at = 0; track_allocations = 1; + struct TOPO_NODE *loaded = topo_node_sqlite_node_load(db, &f->groups, node->node_id); + track_allocations = 0; + assert(loaded && loaded->v4_addrs && loaded->v6_addrs && loaded->reality_socks && attempts > 8); + unsigned total = attempts; + topo_node_destroy(&f->groups, loaded); + for (unsigned nth = 1; nth <= total; nth++) { + size_t before = live_allocations(); + attempts = 0; fail_at = nth; track_allocations = 1; + loaded = topo_node_sqlite_node_load(db, &f->groups, node->node_id); + track_allocations = 0; + assert(!loaded && attempts == nth && live_allocations() == before); + assert(sqlite3_next_stmt(db, NULL) == NULL); + } + fail_at = 0; + assert(sqlite3_exec(db, "DROP TABLE node_addresses", NULL, NULL, NULL) == SQLITE_OK); + size_t before = live_allocations(); + assert(!topo_node_sqlite_node_load(db, &f->groups, node->node_id) && live_allocations() == before); + assert(sqlite3_next_stmt(db, NULL) == NULL); + topo_node_destroy(&f->groups, node); sqlite3_close(db); + DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] SQL bootstrap: %u allocation failures and SQL failure leave no live objects/statements", total); +} + +/* Уже опубликованный local_node и его маршруты переживают любой отказ подготовки новой группы. */ +static void test_local_allocation_failures(struct wire_fixture *f) { + struct CFG_ROUTE_ENTRY v6 = {.ip.family = AF_INET6, .netmask = 64}; + memcpy(&v6.ip.addr.v6, (uint8_t[]){0x20, 1, 0xdb, 8}, 4); + struct CFG_ROUTE_ENTRY v4 = {.next = &v6, .ip.family = AF_INET, .netmask = 24}; + v4.ip.addr.v4.s_addr = htonl(0x0a010000); + struct utun_config config = {.my_subnets = &v4}; f->inst.config = &config; + f->inst.ua = uasync_create(); assert(f->inst.ua); + f->groups.node_registry = queue_new(f->inst.ua, 16, 0, 8, "local_wire_test"); assert(f->groups.node_registry); + assert(sc_generate_keypair(&f->inst.my_keys) == SC_OK); + uint8_t seed[SHA512_DIGEST_LENGTH]; SHA512(f->inst.my_keys.private_key, 32, seed); + memcpy(f->inst.my_ed25519_privkey, seed, 32); + assert(sc_derive_ed25519_pubkey(f->inst.my_keys.private_key, f->inst.my_ed25519_pubkey) == SC_OK); + f->inst.node_id = sc_derive_node_id_from_pubkey(f->inst.my_keys.public_key); + f->inst.rt = route_table_create(f->inst.node_id); assert(f->inst.rt); + assert(!topo_node_init_self(&f->inst)); + f->group.group_type = TOPO_GROUP_TYPE_UTUN; + assert(topo_group_update_my_nodeinfo(&f->inst, &f->group) == 1); + v4.ip.addr.v4.s_addr = htonl(0x0a020000); + attempts = 0; fail_at = 0; track_allocations = 1; + int rc = topo_group_update_my_nodeinfo(&f->inst, &f->group); + track_allocations = 0; + assert(rc == 1 && attempts >= 4); unsigned total = attempts; + struct TOPO_GROUP_NODE* old = f->group.local_node; + struct TOPO_NODE* node = topo_node_registry_find(&f->groups, f->inst.node_id); + uint64_t timestamp = node->timestamp; uint32_t refs = node->group_ref_count; + v4.ip.addr.v4.s_addr = htonl(0x0a030000); + for (unsigned nth = 1; nth <= total; nth++) { + size_t before = live_allocations(); + attempts = 0; fail_at = nth; track_allocations = 1; + rc = topo_group_update_my_nodeinfo(&f->inst, &f->group); + track_allocations = 0; + assert(rc < 0 && attempts == nth && f->group.local_node == old && live_allocations() == before); + assert(node->timestamp == timestamp && node->group_ref_count == refs && !topo_node_verify(node)); + assert(route_lookup(f->inst.rt, 0x0a020001)->v_node_info == old && !route_lookup(f->inst.rt, 0x0a030001)); + } + fail_at = 0; + topo_nodeq_free_group_fields(&f->groups, old); u_free(old); f->group.local_node = NULL; + topo_node_registry_unref(&f->groups, f->inst.node_id); + assert(!f->groups.node_registry->head); queue_free(f->groups.node_registry); f->groups.node_registry = NULL; + route_table_destroy(f->inst.rt); f->inst.rt = NULL; + uasync_destroy(f->inst.ua, 0); f->inst.ua = NULL; f->inst.config = NULL; + DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] local group: %u allocation failures preserve identity, routes and ownership", total); +} +#endif + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG); + size_t before = u_get_allocated_count(); + struct wire_fixture fixture; + fixture_init(&fixture); + test_invalid_packets(&fixture); + test_optional_outputs(&fixture); + test_encoder(&fixture); + test_empty_and_arguments(&fixture); +#ifdef __linux__ + test_allocation_failures(&fixture); + test_bootstrap_allocation_failures(&fixture); + test_local_allocation_failures(&fixture); +#endif + fixture_destroy(&fixture); + assert(u_get_allocated_count() == before); + DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] NODEINFO wire suite finished without leaks"); + return 0; +} diff --git a/tools/etcp_diag_report.py b/tools/etcp_diag_report.py new file mode 100644 index 00000000..641bb54d --- /dev/null +++ b/tools/etcp_diag_report.py @@ -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''' +ETCP: очереди и pacing + +

ETCP: динамика очередей, DATA и ACK

+

Источник: . Данные проверены. Наведите курсор на график для просмотра всех полей среза.

+ + + + +

Очереди приложения/normalizer/send/wait_ack общие для соединения. Flight, pacer, ACK и канал — для выбранного пути. +Блокировки — число проверок выбора, а не время ожидания. Оранжевый фон: BBR PROBE_RTT. +Пики очереди и задержки эмулятора накоплены с последнего сброса его статистики.

+

Исходный срез

+''' + + +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()