From f5f106c682857ada6af2831d73d922174f3d262f Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 21 Oct 2026 15:21:27 +0200 Subject: [PATCH] docs: clarify header APIs and preserve originals in .h1 --- lib/ll_queue.h | 16 +- lib/ll_queue.h1 | 444 ++++++++++++++++++++++++ src/call/call.h | 34 +- src/call/call.h1 | 119 +++++++ src/call/call_video_port.h | 64 +++- src/call/call_video_port.h1 | 51 +++ src/chat/chat_attachment.h | 55 ++- src/chat/chat_attachment.h1 | 46 +++ src/chat/chat_core.h | 126 ++++--- src/chat/chat_core.h1 | 313 +++++++++++++++++ src/chat/chat_event.h | 17 +- src/chat/chat_event.h1 | 104 ++++++ src/chat/group_avatar.h | 40 ++- src/chat/group_avatar.h1 | 32 ++ src/media_delivery/media_delivery.h | 41 ++- src/media_delivery/media_delivery.h1 | 175 ++++++++++ src/media_delivery/media_index.h | 33 +- src/media_delivery/media_index.h1 | 67 ++++ src/member_files/member_files.h | 62 +++- src/member_files/member_files.h1 | 56 +++ src/routing_layer/multicast.h | 24 +- src/routing_layer/multicast.h1 | 219 ++++++++++++ src/stream/stream.h | 111 ++++-- src/stream/stream.h1 | 125 +++++++ src/stream/stream_optimizer.h | 34 +- src/stream/stream_optimizer.h1 | 26 ++ src/stream/stream_playback_clock.h | 22 +- src/stream/stream_playback_clock.h1 | 49 +++ src/transport_layer/auto_socket.h | 41 +-- src/transport_layer/auto_socket.h1 | 95 +++++ src/transport_layer/etcp_connections.h | 67 +++- src/transport_layer/etcp_connections.h1 | 430 +++++++++++++++++++++++ src/transport_layer/etcp_padding.h | 27 +- src/transport_layer/etcp_padding.h1 | 36 ++ src/transport_layer/node_conn_direct.h | 58 ++-- src/transport_layer/node_conn_direct.h1 | 149 ++++++++ src/transport_layer/reality_io.h | 26 +- src/transport_layer/reality_io.h1 | 40 +++ 38 files changed, 3212 insertions(+), 262 deletions(-) create mode 100644 lib/ll_queue.h1 create mode 100644 src/call/call.h1 create mode 100644 src/call/call_video_port.h1 create mode 100644 src/chat/chat_attachment.h1 create mode 100644 src/chat/chat_core.h1 create mode 100644 src/chat/chat_event.h1 create mode 100644 src/chat/group_avatar.h1 create mode 100644 src/media_delivery/media_delivery.h1 create mode 100644 src/media_delivery/media_index.h1 create mode 100644 src/member_files/member_files.h1 create mode 100644 src/routing_layer/multicast.h1 create mode 100644 src/stream/stream.h1 create mode 100644 src/stream/stream_optimizer.h1 create mode 100644 src/stream/stream_playback_clock.h1 create mode 100644 src/transport_layer/auto_socket.h1 create mode 100644 src/transport_layer/etcp_connections.h1 create mode 100644 src/transport_layer/etcp_padding.h1 create mode 100644 src/transport_layer/node_conn_direct.h1 create mode 100644 src/transport_layer/reality_io.h1 diff --git a/lib/ll_queue.h b/lib/ll_queue.h index ae3d878d..783d8116 100644 --- a/lib/ll_queue.h +++ b/lib/ll_queue.h @@ -181,7 +181,7 @@ struct ll_queue { #endif }; -/* ==================== Создание / уничтожение ==================== */ +// ==================== Создание / уничтожение ==================== /** * @brief Создаёт новую пустую очередь. @@ -203,7 +203,7 @@ struct ll_queue* queue_new(struct UASYNC* ua, size_t hash_size, uint16_t index_o */ void queue_free(struct ll_queue* q); -/* ==================== Конфигурация ==================== */ +// ==================== Конфигурация ==================== /** * @brief Устанавливает максимальное количество элементов в очереди. @@ -214,7 +214,7 @@ void queue_free(struct ll_queue* q); */ void queue_set_size_limit(struct ll_queue* q, int lim); -/* ==================== Автозабор элементов ==================== */ +// ==================== Автозабор элементов ==================== /** * @brief Устанавливает коллбэк для автоматического извлечения элементов. @@ -235,7 +235,7 @@ void queue_set_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg) */ void queue_resume_callback(struct ll_queue* q); -/* ==================== Пороговое ожидание (backpressure) ==================== */ +// ==================== Пороговое ожидание (backpressure) ==================== /** * @brief Устанавливает общий порог освобождения очереди. @@ -304,7 +304,7 @@ int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h, */ void queue_waiter_cancel(struct ll_queue* q, struct queue_waiter_handle* h); -/* ==================== Работа с данными ==================== */ +// ==================== Работа с данными ==================== /** * @brief Добавляет элемент в конец очереди (FIFO). @@ -356,7 +356,7 @@ struct ll_entry* queue_data_get(struct ll_queue* q); */ int queue_entry_count(struct ll_queue* q); -/* ==================== Управление памятью элементов ==================== */ +// ==================== Управление памятью элементов ==================== // проверить правильность очереди (корректность цепочки, счетчики элементов и байтов) int queue_check_consistency(struct ll_queue* q); @@ -396,7 +396,7 @@ void queue_entry_free(struct ll_entry* entry); */ void queue_dgram_free(struct ll_entry* entry); -/* ==================== Поиск и удаление ==================== */ +// ==================== Поиск и удаление ==================== /** * @brief Находит элемент по ключу индекса (требуется hash_size > 0 при создании). @@ -426,7 +426,7 @@ struct ll_entry* queue_find_next_by_index(struct ll_queue* q, */ int queue_remove_data(struct ll_queue* q, struct ll_entry* entry); -/* ==================== Утилиты ==================== */ +// ==================== Утилиты ==================== /** * @brief Возвращает суммарный объём данных в очереди. diff --git a/lib/ll_queue.h1 b/lib/ll_queue.h1 new file mode 100644 index 00000000..971a63e3 --- /dev/null +++ b/lib/ll_queue.h1 @@ -0,0 +1,444 @@ +#ifndef LL_QUEUE_H +#define LL_QUEUE_H + +/* Очередь одного uasync-потока; наличие pthread-заголовков не делает её межпоточной. + * put может синхронно вызвать consumer/waiter. Держать владельцев живыми до завершения callbacks, + * waiter handle обнулять целиком и отменять до освобождения; entry и dgram освобождаются отдельно. */ + +#ifdef __cplusplus +extern "C" { +#endif + + +#include // для size_t +#include // для uint64_t +#include "memory_pool.h" // для struct memory_pool + +#ifdef _WIN32 + #include + #include +#else + #include +#endif + +//#define QUEUE_DEBUG 1 // раскомментировать для отладки очереди +//#define QUEUE_THREAD_CHECK 1 // 0 to disable + +/** + * @file ll_queue.h + * @brief Упрощённая двусвязная очередь с поддержкой автозабора элементов через callback, + * порогового ожидания освобождения места, поиска по ID и гибкого управления памятью. + * + * Основные особенности: + * - FIFO-очередь (с возможностью добавления в начало для приоритета) + * - Автоматический вызов callback при появлении элементов (один элемент за раз) + * - Обязательный вызов queue_resume_callback после обработки элемента + * - Поддержка пулов памяти для entry и отдельно для dgram + * - Хеш-таблица для быстрого поиска по ID (опционально) + * - Связный список waiters для backpressure c общим порогом (round-robin) + * - Использует uasync для отложенного возобновления callback'ов (без рекурсии в стеке) + * + * @note Важные правила использования автозабора (queue_set_callback): + * 1. Коллбэк получает управление только если в очереди есть элементы И !callback_suspended + * 2. Внутри коллбэка ОБЯЗАТЕЛЬНО: queue_data_get() → обработать элемент → queue_resume_callback() + * 3. Без вызова queue_resume_callback очередь навсегда застрянет + * 4. Не извлекайте элементы вручную вне коллбэка — это нарушит логику + * 5. queue_data_get() автоматически suspend'ит коллбэки для предотвращения рекурсии + * + * @note Память: + * - queue_free() освобождает структуру очереди, хеш-таблицу и список waiters + * (handle->internal каждого ожидающего сбрасывается в NULL) + * - Все элементы должны быть извлечены через queue_data_get() и освобождены через queue_entry_free() + * - dgram освобождается отдельно через queue_dgram_free() или кастомную dgram_free_fn + */ + +// Forward declarations +struct ll_queue; +struct queue_waiter_handle; + +/** + * @struct ll_entry + * @brief Элемент очереди (переменного размера). + * + * Память выделяется одним блоком: [struct ll_entry + data[size]]. + * Поле dgram — отдельный блок (malloc / пул / внешняя память). + */ +struct ll_entry { + char* name; + struct ll_queue* owner; // Владелец элемента; NULL после извлечения + struct ll_entry* next; // Следующий элемент в очереди + struct ll_entry* prev; // Предыдущий элемент в очереди + uint16_t size; // Размер пользовательского буфера data[] (байт) + uint16_t len; // Актуальная длина данных в dgram + uint16_t memlen; // Выделенный размер под dgram + uint16_t int_len; // Внутреннее (не использовать) + uint8_t* dgram; // Указатель на данные пакета + void (*dgram_free_fn)(uint8_t* data); // Кастомная функция освобождения dgram + struct memory_pool* dgram_pool; // Пул для dgram (если используется) + uint32_t index_hash; // Хеш по первым 4 байтам индекса + struct ll_entry* hash_next; // Следующий в хеш-цепочке + struct memory_pool* pool; // Пул, из которого выделен сам entry (NULL = malloc) + uint8_t data[0]; // Гибкий массив пользовательских данных (размер = size) +}; + +/** + * @typedef queue_callback_fn + * @brief Коллбэк автозабора элементов из очереди. + * @param q указатель на очередь + * @param arg пользовательский аргумент (из queue_set_callback) + * + * @note Внутри функции: + * - Вызвать queue_data_get(q) для получения элемента + * - Обработать элемент (можно асинхронно) + * - После завершения обработки вызвать queue_resume_callback(q) + */ +typedef void (*queue_callback_fn)(struct ll_queue* q, void* arg); + +/** + * @typedef queue_threshold_callback_fn + * @brief Одноразовый коллбэк при достижении порога очереди. + * @param q указатель на очередь + * @param arg пользовательский аргумент + */ +typedef void (*queue_threshold_callback_fn)(struct ll_queue* q, void* arg); + +/** + * @struct queue_waiter + * @brief Внутренний узел в связном списке ожидающих (очередь владеет, аллоцирует/освобождает сама). + */ +struct queue_waiter { + struct queue_waiter* next; ///< Следующий в списке ожидающих + struct queue_waiter_handle* handle; ///< Обратная ссылка на публичную структуру + queue_threshold_callback_fn callback; + void* callback_arg; +}; + +/** + * @struct queue_waiter_handle + * @brief Публичная управляющая структура (встраивается в структуру вызывающей стороны). + * + * Вызывающая сторона: + * 1. Объявляет поле struct queue_waiter_handle в своей структуре (= {0} или u_calloc) + * 3. Вызывает queue_waiter_wait() когда хочет дождаться освобождения очереди + * 4. Вызывает queue_waiter_cancel() при деинициализации для отмены ожидания + */ +struct queue_waiter_handle { + struct queue_waiter* internal; ///< NULL = не ждём; иначе — внутренний узел + struct ll_queue* defer_q; + queue_threshold_callback_fn defer_cb; + void* defer_arg; + void* call_soon_id; ///< хендл отложенного вызова (для отмены) + struct queue_waiter_handle* defer_next; ///< список отложенных уведомлений очереди +}; + +/** + * @struct ll_queue + * @brief Структура очереди. + */ +struct ll_queue { + char* name; + struct ll_entry* head; // Голова очереди (отсюда извлекаем) + struct ll_entry* tail; // Хвост очереди (сюда добавляем) + int count; // Текущее количество элементов + size_t total_bytes; // Суммарный объём данных (сумма int_len) + int size_limit; // Максимальное количество элементов (-1 = без лимита) + + queue_callback_fn callback; // Коллбэк автозабора + void* callback_arg; // Аргумент коллбэка + int callback_suspended; // 1 = коллбэки временно приостановлены + + void* resume_timeout_id; // ID таймера uasync для отложенного resume + struct UASYNC* ua; // Экземпляр uasync (обязателен для таймеров) + + struct queue_waiter* waiter_head; // Голова списка ожидающих + struct queue_waiter* waiter_tail; // Хвост списка ожидающих + struct queue_waiter_handle* deferred_waiters; // Уже запланированные уведомления + int threshold_max_packets; // Общий порог: макс. кол-во элементов (по умолчанию 0) + size_t threshold_max_bytes; // Общий порог: макс. объём данных (0 = не проверять) + int waiter_defer; // 0=immediate callback, 1=defer via uasync_call_soon + int callback_defer; // 0=immediate callback из put (default), 1=defer via uasync_call_soon (очистка стека) + + queue_callback_fn empty_callback; // одноразовый коллбэк при опустошении очереди + void* empty_callback_arg; + void* empty_call_soon_id; // handle от uasync_call_soon + + queue_callback_fn on_get; // коллбэк после каждого queue_data_get (deferred) + void* on_get_arg; + void* on_get_call_soon_id; + size_t on_get_pending; + + struct ll_entry** hash_table; // Хеш-таблица для поиска по id (если hash_size > 0) + size_t hash_size; // Размер хеш-таблицы + uint16_t index_offset; // Смещение индекса в data[] (для всех entry очереди) + uint16_t index_size; // Размер индекса (0 = без индекса) + +#ifdef QUEUE_THREAD_CHECK +#ifdef _WIN32 + DWORD owner_thread; +#else + pthread_t owner_thread; +#endif +#endif +}; + +/* ==================== Создание / уничтожение ==================== */ + +/** + * @brief Создаёт новую пустую очередь. + * @param ua экземпляр uasync (обязателен для отложенного resume коллбэков) + * @param hash_size размер хеш-таблицы (0 = поиск по id отключён) + * @param index_offset смещение ключа индекса в data[] каждого entry (0 если hash_size=0) + * @param index_size размер ключа индекса (0 если hash_size=0) + * @param name имя очереди для отладки + * @return указатель на очередь или NULL при ошибке выделения памяти + */ +struct ll_queue* queue_new(struct UASYNC* ua, size_t hash_size, uint16_t index_offset, uint16_t index_size, char* name); + +/** + * @brief Освобождает структуру очереди. + * @param q очередь + * + * @warning НЕ ОСВОБОЖДАЕТ элементы в очереди! + * Их нужно извлечь через queue_data_get() и освободить через queue_entry_free(). + */ +void queue_free(struct ll_queue* q); + +/* ==================== Конфигурация ==================== */ + +/** + * @brief Устанавливает максимальное количество элементов в очереди. + * @param q очередь + * @param lim лимит (-1 = без ограничения) + * + * При превышении лимита новые элементы автоматически освобождаются. + */ +void queue_set_size_limit(struct ll_queue* q, int lim); + +/* ==================== Автозабор элементов ==================== */ + +/** + * @brief Устанавливает коллбэк для автоматического извлечения элементов. + * @param q очередь + * @param cbk_fn функция-коллбэк + * @param arg пользовательский аргумент + * + * Коллбэк вызывается автоматически при наличии элементов и разрешённом состоянии. + */ +void queue_set_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg); + +/** + * @brief Возобновляет работу коллбэка после обработки элемента в следующем event loop. + * @param q очередь + * + * @warning ОБЯЗАТЕЛЬНО вызывать после завершения обработки элемента в коллбэке. + * Иначе автозабор элементов навсегда остановится. + */ +void queue_resume_callback(struct ll_queue* q); + +/* ==================== Пороговое ожидание (backpressure) ==================== */ + +/** + * @brief Устанавливает общий порог освобождения очереди. + * @param q очередь + * @param max_packets максимальное количество элементов (условие: count <= max_packets) + * @param max_bytes максимальный объём данных (0 = не проверять) + * + * Порог — общий на всю очередь. Все waiters в связном списке используют этот порог. + * При достижении порога вызывается следующий waiter из списка (FIFO / round-robin). + * По умолчанию: max_packets=0, max_bytes=0 (пустая очередь). + */ +void queue_set_threshold(struct ll_queue* q, int max_packets, size_t max_bytes); + +void queue_set_waiter_defer(struct ll_queue* q, int enable); + +/** + * @brief Управляет способом вызова коллбэка автозабора при добавлении элемента. + * @param q очередь + * @param enable 0 — первый запуск коллбэка из queue_data_put* вызывается синхронно (по умолчанию); + * 1 — первый запуск откладывается через uasync_call_soon (очищает стек вызывающего). + * + * @note Влияет только на первый запуск из put. Повторные запуски (queue_resume_callback) + * всегда идут через uasync_call_soon. + */ +void queue_set_callback_defer(struct ll_queue* q, int enable); + +/** + * @brief Устанавливает одноразовый коллбэк при опустошении очереди (count==0). + * @param q очередь + * @param cbk_fn коллбэк (NULL для отмены) + * @param arg пользовательский аргумент + * + * Коллбэк вызывается асинхронно (через uasync_call_soon) когда count становится 0. + * После вызова сбрасывается — для повторного использования нужно вызвать снова. + */ +void queue_set_empty_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg); + +/** + * @brief Устанавливает коллбэк вызываемый после каждого queue_data_get (deferred через call_soon). + * @param q очередь + * @param cbk_fn коллбэк (NULL для отмены) + * @param arg пользовательский аргумент + */ +void queue_set_on_get(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg); + +/** + * @brief Регистрирует ожидание освобождения очереди до общего порога. + * @param q очередь + * @param h полностью обнулённый handle: `struct queue_waiter_handle h = {0}` + * @param callback функция, вызываемая при достижении порога + * @param arg аргумент коллбэка + * @return 1 — условие выполнено (callback вызван или отложен), 0 — ожидает порог, -1 — ошибка + * + * Если handle уже ожидает или имеет отложенный callback — старое ожидание отменяется. + * При queue_free отменяются также уже запланированные уведомления. + */ +int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h, + queue_threshold_callback_fn callback, void* arg); + +/** + * @brief Отменяет ожидание (удаляет из списка, если зарегистрирован). + * @param q очередь + * @param h указатель на handle + * + * Безопасно вызывать в любом состоянии: если waiter не зарегистрирован — ничего не делает. + */ +void queue_waiter_cancel(struct ll_queue* q, struct queue_waiter_handle* h); + +/* ==================== Работа с данными ==================== */ + +/** + * @brief Добавляет элемент в конец очереди (FIFO). + * @param q очередь + * @param entry элемент + * @return 0 — успех, -1 — превышен лимит (элемент освобождён) + */ +int queue_data_put(struct ll_queue* q, struct ll_entry* entry); + +/** + * @brief Добавляет элемент в начало очереди (LIFO, высокий приоритет). + * @param q очередь + * @param entry элемент + * @return 0 — успех, -1 — превышен лимит (элемент освобождён) + */ +int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry); + +/** + * @brief Добавляет элемент в конец очереди с хеш-индексом (использует index_offset/index_size из очереди). + * @param q очередь + * @param entry элемент + * @return 0 — успех, -1 — ошибка + */ +int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry); + +/** + * @brief Добавляет элемент в начало очереди с хеш-индексом (использует index_offset/index_size из очереди). + * @param q очередь + * @param entry элемент + * @return 0 — успех, -1 — ошибка + */ +int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry); + + +/** + * @brief Извлекает элемент из начала очереди. + * @param q очередь + * @return элемент или NULL, если очередь пуста + * + * @note Автоматически приостанавливает коллбэки (callback_suspended = 1) + * После обработки элемента необходимо вызвать queue_resume_callback(). + */ +struct ll_entry* queue_data_get(struct ll_queue* q); + +/** + * @brief Возвращает текущее количество элементов в очереди. + * @param q очередь + * @return количество элементов + */ +int queue_entry_count(struct ll_queue* q); + +/* ==================== Управление памятью элементов ==================== */ + +// проверить правильность очереди (корректность цепочки, счетчики элементов и байтов) +int queue_check_consistency(struct ll_queue* q); + +/** + * @brief Выделяет элемент с отдельным буфером dgram (malloc). + * @param len требуемый размер dgram + * @return элемент или NULL при ошибке + */ +struct ll_entry* ll_alloc_lldgram(uint16_t len); + +/** + * @brief Создаёт элемент с пользовательским буфером data[] (malloc одним блоком). + * @param data_size размер data[] + * @return элемент или NULL при ошибке + */ +struct ll_entry* queue_entry_new(size_t data_size); + +/** + * @brief Создаёт элемент из пула памяти. + * @param pool пул + * @return элемент или NULL, если пул пуст + */ +struct ll_entry* queue_entry_new_from_pool(struct memory_pool* pool); + +/** + * @brief Освобождает структуру элемента (возвращает в пул или free). + * @param entry элемент + * + * @note НЕ освобождает dgram! Используйте queue_dgram_free() отдельно. + */ +void queue_entry_free(struct ll_entry* entry); + +/** + * @brief Освобождает буфер dgram элемента. + * @param entry элемент + */ +void queue_dgram_free(struct ll_entry* entry); + +/* ==================== Поиск и удаление ==================== */ + +/** + * @brief Находит элемент по ключу индекса (требуется hash_size > 0 при создании). + * @param q очередь (размер ключа берётся из q->index_size) + * @param index_key искомый ключ + * @return элемент или NULL + */ +struct ll_entry* queue_find_data_by_index(struct ll_queue* q, + const void* index_key); + +/** + * @brief Находит следующий элемент с тем же ключом (продолжение поиска). + * @param q очередь (размер ключа берётся из q->index_size) + * @param index_key искомый ключ + * @param prev_entry предыдущий найденный элемент (от queue_find_data_by_index или предыдущего queue_find_next_by_index) + * @return следующий элемент с тем же ключом или NULL + */ +struct ll_entry* queue_find_next_by_index(struct ll_queue* q, + const void* index_key, + struct ll_entry* prev_entry); + +/** + * @brief Удаляет элемент из очереди (не освобождает память). + * @param q очередь + * @param entry элемент + * @return 0 — успех, -1 — элемент не найден в очереди + */ +int queue_remove_data(struct ll_queue* q, struct ll_entry* entry); + +/* ==================== Утилиты ==================== */ + +/** + * @brief Возвращает суммарный объём данных в очереди. + * @param q очередь + * @return количество байт + */ +static inline size_t queue_total_bytes(struct ll_queue* q) { + return q->total_bytes; +} + + +#ifdef __cplusplus +} +#endif +#endif // LL_QUEUE_H diff --git a/src/call/call.h b/src/call/call.h index 4a3fda35..9ee56954 100644 --- a/src/call/call.h +++ b/src/call/call.h @@ -23,26 +23,32 @@ struct UTUN_INSTANCE; extern "C" { #endif -/* ── жизненный цикл ── */ +// ── жизненный цикл ── int call_init(struct UTUN_INSTANCE* inst); /* из utun_instance.c после dm_core_init */ + void call_destroy(struct UTUN_INSTANCE* inst); /* перед dm_core_destroy */ -/* ── сигналинг (uasync-поток) ── */ +// ── сигналинг (uasync-поток) ── /* Caller: ch_id — группа либо "dm:" (BGP-группа выбирается при запуске). * out_call_id — случайный uint64, генерит вызывающий. */ void call_start (struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id, uint64_t out_call_id); -/* Callee: ответить/отклонить. hangup — любая сторона в любой момент. */ +// Callee: ответить/отклонить. hangup — любая сторона в любой момент. void call_accept (struct UTUN_INSTANCE* inst, uint64_t call_id); + void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id); + void call_hangup (struct UTUN_INSTANCE* inst, uint64_t call_id); -/* Проверка занятости обычным звонком; только uasync, включая входящий/исходящий. */ + +// Проверка занятости обычным звонком; только uasync, включая входящий/исходящий. int call_busy(struct UTUN_INSTANCE* inst); -/* Продолжить исходящий вызов после предупреждения о телефонной занятости. Только uasync. */ + +// Продолжить исходящий вызов после предупреждения о телефонной занятости. Только uasync. void call_continue(struct UTUN_INSTANCE* inst, uint64_t call_id); -/* Независимые настройки входящих вызовов; запрет выбранного типа приоритетнее проверки занятости. */ + +// Независимые настройки входящих вызовов; запрет выбранного типа приоритетнее проверки занятости. enum call_policy { CALL_POLICY_PUBLISH_PHONE = 1, CALL_POLICY_WARN_PHONE = 2, CALL_POLICY_REJECT_PHONE = 4, CALL_POLICY_BLOCK_CALLS = 8, CALL_POLICY_BLOCK_RADIO = 16 @@ -50,14 +56,15 @@ enum call_policy { /* Живое состояние ОС: 0=неизвестно, 1=свободен, 2=занят; policy — маска call_policy. * Только uasync. Изменение статуса публикуется в BGP, если включён PUBLISH_PHONE. */ void call_set_phone_state(struct UTUN_INSTANCE* inst, int state, unsigned policy); + /* Флаги BGP PHONE_KNOWN/BUSY с учётом звонка приложения и разрешения публикации. * Desktop без звонка свободен; у mobile состояние ОС может быть неизвестно. Только uasync. */ uint8_t call_phone_status(struct UTUN_INSTANCE* inst); -/* Read-only онлайн-проверка (GUI может звать из любого потока). */ +// Read-only онлайн-проверка (GUI может звать из любого потока). int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id); -/* ── медиа (релей Opus-кадров, uasync-поток) ── */ +// ── медиа (релей Opus-кадров, uasync-поток) ── /* Отправить закодированный Opus-кадр пиру (оборачивает в CALL_MEDIA). * Только когда звонок ACTIVE. seq/ts выставляются автоматически. */ @@ -68,6 +75,7 @@ int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* * Используется headless-аудиосокетом; opus заимствован до возврата callback. */ typedef void (*call_media_recv_fn)(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* opus, int len, void* arg); + void call_set_media_recv_cb(struct UTUN_INSTANCE* inst, call_media_recv_fn fn, void* arg); /* ── аудио-движок (опционально, GUI-сборки) ── @@ -76,7 +84,7 @@ void call_set_media_recv_cb(struct UTUN_INSTANCE* inst, call_media_recv_fn fn, v * и get_stats для события CALL_STATS. Демон без аудио (headless) — ops не ставит. */ struct call_audio_ops { - /* uasync-поток: принятый Opus-кадр пира (декод + jitter-буфер). */ + // uasync-поток: принятый Opus-кадр пира (декод + jitter-буфер). void (*on_media)(struct UTUN_INSTANCE* inst, uint64_t call_id, uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing); /* uasync-поток: текущие метрики джиттер-буфера (buffer_ms, tempo_x100, @@ -86,7 +94,7 @@ struct call_audio_ops { }; void call_set_audio_ops(struct UTUN_INSTANCE* inst, const struct call_audio_ops* ops); -/* Отправить Opus-кадр из ДРУГОГО потока (GUI-аудио): post в uasync через trampoline. */ +// Отправить Opus-кадр из ДРУГОГО потока (GUI-аудио): post в uasync через trampoline. struct call_media_arg { struct UTUN_INSTANCE* inst; uint64_t call_id; @@ -96,7 +104,7 @@ struct call_media_arg { }; void call_send_media_trampoline(void* arg); -/* ── trampoline-структуры для gui_bridge_post_uasync_fn / uasync_post ── */ +// ── trampoline-структуры для gui_bridge_post_uasync_fn / uasync_post ── struct call_start_arg { struct UTUN_INSTANCE* inst; @@ -111,9 +119,13 @@ struct call_id_arg { }; void call_start_trampoline (void* arg); + void call_accept_trampoline (void* arg); + void call_decline_trampoline(void* arg); + void call_hangup_trampoline (void* arg); + void call_continue_trampoline(void* arg); #ifdef __cplusplus diff --git a/src/call/call.h1 b/src/call/call.h1 new file mode 100644 index 00000000..b7fdfa90 --- /dev/null +++ b/src/call/call.h1 @@ -0,0 +1,119 @@ +// call.h — P2P аудио-звонок (сигналинг + медиа), per-instance +// +// Модуль оперирует (group_id, peer_node_id): звонок можно инициировать из +// CHAT-группы или из DM (group_id — группа маршрутизации, как в dm_core). +// Сигналинг, аудио и видео идут через etcp_router с обязательными ROUTE_CRYPTO_SIGN|ENCRYPT; соединение +// оптимизируется через параллельные пробы conn_mgr (DIRECT/REVERSE/INDIRECT). +// +// Все функции — в uasync-потоке. GUI/headless вызывают через trampoline +// (gui_bridge_post_uasync_fn / uasync_post). +#ifndef CALL_H +#define CALL_H + +#include +#include "call_jitter.h" +#include "call_clock.h" + +struct UTUN_INSTANCE; + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── жизненный цикл ── */ + +int call_init(struct UTUN_INSTANCE* inst); /* из utun_instance.c после dm_core_init */ +void call_destroy(struct UTUN_INSTANCE* inst); /* перед dm_core_destroy */ + +/* ── сигналинг (uasync-поток) ── */ + +/* Caller: ch_id — группа либо "dm:" (BGP-группа выбирается при запуске). + * out_call_id — случайный uint64, генерит вызывающий. */ +void call_start (struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id, uint64_t out_call_id); + +/* Callee: ответить/отклонить. hangup — любая сторона в любой момент. */ +void call_accept (struct UTUN_INSTANCE* inst, uint64_t call_id); +void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id); +void call_hangup (struct UTUN_INSTANCE* inst, uint64_t call_id); +/* Проверка занятости обычным звонком; только uasync, включая входящий/исходящий. */ +int call_busy(struct UTUN_INSTANCE* inst); +/* Продолжить исходящий вызов после предупреждения о телефонной занятости. Только uasync. */ +void call_continue(struct UTUN_INSTANCE* inst, uint64_t call_id); +/* Независимые настройки входящих вызовов; запрет выбранного типа приоритетнее проверки занятости. */ +enum call_policy { + CALL_POLICY_PUBLISH_PHONE = 1, CALL_POLICY_WARN_PHONE = 2, CALL_POLICY_REJECT_PHONE = 4, + CALL_POLICY_BLOCK_CALLS = 8, CALL_POLICY_BLOCK_RADIO = 16 +}; +/* Живое состояние ОС: 0=неизвестно, 1=свободен, 2=занят; policy — маска call_policy. + * Только uasync. Изменение статуса публикуется в BGP, если включён PUBLISH_PHONE. */ +void call_set_phone_state(struct UTUN_INSTANCE* inst, int state, unsigned policy); +/* Флаги BGP PHONE_KNOWN/BUSY с учётом звонка приложения и разрешения публикации. + * Desktop без звонка свободен; у mobile состояние ОС может быть неизвестно. Только uasync. */ +uint8_t call_phone_status(struct UTUN_INSTANCE* inst); + +/* Read-only онлайн-проверка (GUI может звать из любого потока). */ +int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id); + +/* ── медиа (релей Opus-кадров, uasync-поток) ── */ + +/* Отправить закодированный Opus-кадр пиру (оборачивает в CALL_MEDIA). + * Только когда звонок ACTIVE. seq/ts выставляются автоматически. */ +int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* opus, int len, + const struct call_capture_time* timing); + +/* Коллбэк приёма медиа: вызывается в uasync-потоке на каждый Opus-кадр пира. + * Используется headless-аудио-сокетом (и в фазе 3 — декодером GUI). */ +typedef void (*call_media_recv_fn)(struct UTUN_INSTANCE* inst, uint64_t call_id, + const uint8_t* opus, int len, void* arg); +void call_set_media_recv_cb(struct UTUN_INSTANCE* inst, call_media_recv_fn fn, void* arg); + +/* ── аудио-движок (опционально, GUI-сборки) ── + * Сервисный аудио-контур (codec + джиттер-буфер + тоны) регистрируется через + * call_set_audio_ops. call.c зовёт on_media на каждый принятый медиа-кадр (с seq/ts) + * и get_stats для события CALL_STATS. Демон без аудио (headless) — ops не ставит. */ + +struct call_audio_ops { + /* uasync-поток: принятый Opus-кадр пира (декод + jitter-буфер). */ + void (*on_media)(struct UTUN_INSTANCE* inst, uint64_t call_id, + uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing); + /* uasync-поток: текущие метрики джиттер-буфера (buffer_ms, tempo_x100, + * dropped/underruns — опционально, NULL допустим). */ + int (*get_stats)(uint64_t call_id, int* buffer_ms, int* tempo_x100, + uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range); +}; +void call_set_audio_ops(struct UTUN_INSTANCE* inst, const struct call_audio_ops* ops); + +/* Отправить Opus-кадр из ДРУГОГО потока (GUI-аудио): post в uasync через trampoline. */ +struct call_media_arg { + struct UTUN_INSTANCE* inst; + uint64_t call_id; + uint16_t len; + struct call_capture_time timing; + uint8_t opus[256]; +}; +void call_send_media_trampoline(void* arg); + +/* ── trampoline-структуры для gui_bridge_post_uasync_fn / uasync_post ── */ + +struct call_start_arg { + struct UTUN_INSTANCE* inst; + char ch_id[64]; + uint64_t peer_node_id; + uint64_t call_id; +}; + +struct call_id_arg { + struct UTUN_INSTANCE* inst; + uint64_t call_id; +}; + +void call_start_trampoline (void* arg); +void call_accept_trampoline (void* arg); +void call_decline_trampoline(void* arg); +void call_hangup_trampoline (void* arg); +void call_continue_trampoline(void* arg); + +#ifdef __cplusplus +} +#endif +#endif /* CALL_H */ diff --git a/src/call/call_video_port.h b/src/call/call_video_port.h index d8701de2..91cbef5c 100644 --- a/src/call/call_video_port.h +++ b/src/call/call_video_port.h @@ -1,8 +1,8 @@ -/* Один платформенный видеозвонок процесса. Attach/detach — uasync; остальные API - * потокобезопасны. Detach отменяет post до уничтожения ядра. Worker не хранит inst. - * Один TX mailbox (до восьми кадров, каждый <=2MiB) + восемь RX кадров <=4MiB; - * владение pull.data передаётся через u_free. TX не ждёт накопления пачки. - * Никаких callbacks в UI: status/pull читает worker. Кодирование не блокирует uasync. */ +/* call_video_port — обмен готовыми видеокадрами между платформенными workers и ядром звонка. + * Attach подключает ядро; для активного call_id: open → send/pull/status → close. Перед отправкой настроить encoder через format. + * Attach/detach — uasync; остальные API потокобезопасны. До detach остановить пользователей порта. + * Send копирует кадры; pull передаёт буфер вызывающему, освобождение — u_free(). Команды исполняются в uasync. + * В процессе один порт: TX до восьми кадров по 2 МиБ, RX до восьми кадров и 4 МиБ суммарно. */ #ifndef CALL_VIDEO_PORT_H #define CALL_VIDEO_PORT_H #include "call_video.h" @@ -11,40 +11,78 @@ struct UTUN_INSTANCE; extern "C" { #endif struct call_video_port_status { - struct call_video_caps peer; - uint64_t key_serial, rate_generation, rate_request, configured_generation; - uint32_t rate_bps; - uint32_t minimum_bps, maximum_bps; - int have_peer, error; + struct call_video_caps peer; // принятые возможности кодеков пира + uint64_t key_serial, rate_generation, rate_request, configured_generation; // счётчик запросов KEY, ID команды битрейта и настроенное поколение + uint32_t rate_bps; // запрошенный кодеру битрейт, бит/с; результат передать через applied + uint32_t minimum_bps, maximum_bps; // действующий диапазон регулятора, бит/с + int have_peer, error; // возможности пира получены / настройка завершилась ошибкой int can_encode; // Новые raw входы допустимы; готовый TX всё равно сохраняется и повторяется. int tx_idle; // Mailbox свободен; новый формат не обгонит кадры прежнего поколения в mailbox. - uint64_t decoded_generation; - int decoded_accepted; + uint64_t decoded_generation; // поколение, для которого удалённый decoder сообщил результат + int decoded_accepted; // 1 — удалённый decoder принял поколение, 0 — отказ unsigned rx_frames; // Полные encoded кадры порта; pending decoder/GUI учитывает RX worker. }; +// ── Привязка к ядру ── + +// Подключить единственное ядро к порту; 0/-1. int call_video_port_attach(struct UTUN_INSTANCE *inst); + +// Отменить ожидающий post и освободить очереди; выполнять до уничтожения ядра. void call_video_port_detach(struct UTUN_INSTANCE *inst); + +// ── Открытие и закрытие видеотрека ── + +// Скопировать локальные capabilities и запланировать настройку видеотрека; 0/-1, не подтверждение готовности пира. int call_video_port_open(uint64_t id, const struct call_video_caps *caps); + +// Закрыть текущий id и очистить его очереди; чужой id игнорируется. void call_video_port_close(uint64_t id); + +// ── Состояние и уведомления ── + +// Скопировать текущее состояние; 0 — снимок получен, -1 — нет активного id/ядра или out. int call_video_port_status(uint64_t id, struct call_video_port_status *out); + /* Неблокирующий накопительный счётчик полных принятых RX кадров текущего звонка. * Pull не уменьшает его. Для общего запаса вычитаются показанные/отклонённые кадры. * 0 для закрытого/другого звонка; новый open после остановки readers начинает с нуля. */ uint64_t call_video_port_received(uint64_t id); + /* Уведомление RX без обращения к port API из callback; вызывается под mutex порта. * 1 — установлен, 0 — звонок ещё не открыт/уже закрыт. NULL снимает callback до освобождения arg. */ int call_video_port_set_wake(uint64_t id, void (*wake)(void *), void *arg); + +// ── Настройка кодера и исходящие кадры ── + +// Передать фактический формат и битрейт открытого encoder; формат копируется. 0 — команда принята, -1 — отказ. int call_video_port_format(uint64_t id, const struct call_video_format *format, uint32_t bps); + +// Подтвердить команду status.rate_generation/rate_request: bps — применённый битрейт, 0 — отказ encoder. 0/-1. int call_video_port_applied(uint64_t id, uint64_t generation, uint64_t request, uint32_t bps); + #define CALL_VIDEO_PORT_TX_FRAMES 8u #define CALL_VIDEO_PORT_TX_BYTES (CALL_VIDEO_PORT_TX_FRAMES * CALL_VIDEO_MAX_FRAME) /* Атомарно копирует готовую пачку одного звонка. 1 — принята целиком, 0 — mailbox занят, - * -1 — ошибка; на 0/-1 ни один кадр не принят. count: 1..CALL_VIDEO_PORT_TX_FRAMES. */ + * -1 — ошибка; на 0/-1 ни один кадр не принят. count: 1..CALL_VIDEO_PORT_TX_FRAMES. + * Входные кадры всегда остаются у вызывающего. При 0 повторить ту же пачку после освобождения порта. */ int call_video_port_send(const struct call_video_frame *frames, unsigned count); + +// ── Входящие кадры ── + +// Забрать старейший полный RX-кадр: 1 — получен, out->data освободить через u_free; 0 — кадра нет, out не меняется. int call_video_port_pull(uint64_t id, struct call_video_frame *out); + +// ── Управление качеством и восстановлением ── + +// Запланировать запрос нового KEY удалённому encoder после потери опорных кадров. void call_video_port_request_key(uint64_t id); + +// Задать диапазон битрейта в бит/с: 300000..4000000, кратно 1000; 0 — команда принята, -1 — отказ. int call_video_port_range(uint64_t id, uint32_t minimum_bps, uint32_t maximum_bps); + +// Сообщить результат запуска decoder поколения generation: accepted=0/1; 0 — команда принята, -1 — отказ. int call_video_port_decoded(uint64_t id, uint64_t generation, int accepted); + #ifdef __cplusplus } #endif diff --git a/src/call/call_video_port.h1 b/src/call/call_video_port.h1 new file mode 100644 index 00000000..d8701de2 --- /dev/null +++ b/src/call/call_video_port.h1 @@ -0,0 +1,51 @@ +/* Один платформенный видеозвонок процесса. Attach/detach — uasync; остальные API + * потокобезопасны. Detach отменяет post до уничтожения ядра. Worker не хранит inst. + * Один TX mailbox (до восьми кадров, каждый <=2MiB) + восемь RX кадров <=4MiB; + * владение pull.data передаётся через u_free. TX не ждёт накопления пачки. + * Никаких callbacks в UI: status/pull читает worker. Кодирование не блокирует uasync. */ +#ifndef CALL_VIDEO_PORT_H +#define CALL_VIDEO_PORT_H +#include "call_video.h" +struct UTUN_INSTANCE; +#ifdef __cplusplus +extern "C" { +#endif +struct call_video_port_status { + struct call_video_caps peer; + uint64_t key_serial, rate_generation, rate_request, configured_generation; + uint32_t rate_bps; + uint32_t minimum_bps, maximum_bps; + int have_peer, error; + int can_encode; // Новые raw входы допустимы; готовый TX всё равно сохраняется и повторяется. + int tx_idle; // Mailbox свободен; новый формат не обгонит кадры прежнего поколения в mailbox. + uint64_t decoded_generation; + int decoded_accepted; + unsigned rx_frames; // Полные encoded кадры порта; pending decoder/GUI учитывает RX worker. +}; +int call_video_port_attach(struct UTUN_INSTANCE *inst); +void call_video_port_detach(struct UTUN_INSTANCE *inst); +int call_video_port_open(uint64_t id, const struct call_video_caps *caps); +void call_video_port_close(uint64_t id); +int call_video_port_status(uint64_t id, struct call_video_port_status *out); +/* Неблокирующий накопительный счётчик полных принятых RX кадров текущего звонка. + * Pull не уменьшает его. Для общего запаса вычитаются показанные/отклонённые кадры. + * 0 для закрытого/другого звонка; новый open после остановки readers начинает с нуля. */ +uint64_t call_video_port_received(uint64_t id); +/* Уведомление RX без обращения к port API из callback; вызывается под mutex порта. + * 1 — установлен, 0 — звонок ещё не открыт/уже закрыт. NULL снимает callback до освобождения arg. */ +int call_video_port_set_wake(uint64_t id, void (*wake)(void *), void *arg); +int call_video_port_format(uint64_t id, const struct call_video_format *format, uint32_t bps); +int call_video_port_applied(uint64_t id, uint64_t generation, uint64_t request, uint32_t bps); +#define CALL_VIDEO_PORT_TX_FRAMES 8u +#define CALL_VIDEO_PORT_TX_BYTES (CALL_VIDEO_PORT_TX_FRAMES * CALL_VIDEO_MAX_FRAME) +/* Атомарно копирует готовую пачку одного звонка. 1 — принята целиком, 0 — mailbox занят, + * -1 — ошибка; на 0/-1 ни один кадр не принят. count: 1..CALL_VIDEO_PORT_TX_FRAMES. */ +int call_video_port_send(const struct call_video_frame *frames, unsigned count); +int call_video_port_pull(uint64_t id, struct call_video_frame *out); +void call_video_port_request_key(uint64_t id); +int call_video_port_range(uint64_t id, uint32_t minimum_bps, uint32_t maximum_bps); +int call_video_port_decoded(uint64_t id, uint64_t generation, int accepted); +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/chat/chat_attachment.h b/src/chat/chat_attachment.h index cd5c4ae2..8a945d98 100644 --- a/src/chat/chat_attachment.h +++ b/src/chat/chat_attachment.h @@ -1,8 +1,8 @@ -/* Зашифрованные групповые вложения и ручная выдача ключей. - * Все API кроме identity — uasync-поток. Ключи, запросы и outbox локальны. - * init после media_async; quiesce до его destroy, destroy после workers. - * Отдельный router-сервис требует E2E-шифрование и подпись каждого пакета. - * Очереди переживают restart; для доставки нужен живой маршрут обеих сторон. */ +/* chat_attachment — вложения канала с шифрованием и ручной выдачей ключа автором. + * send готовит контейнер и публикует сообщение; downloaded/want запускают получение ключа и расшифровку. + * Автор отвечает на запрос через decide. Ключи и очередь доставки сохраняются в локальной БД. + * Все вызовы, кроме identity, — uasync. init после media_async; quiesce до остановки workers, destroy после них. + * Обмен ключами подписан и зашифрован; для доставки нужен живой маршрут обеих сторон. */ #ifndef CHAT_ATTACHMENT_H #define CHAT_ATTACHMENT_H #include "../media_async/attachment.h" @@ -11,25 +11,51 @@ extern "C" { #endif struct UTUN_INSTANCE; + +// ── Жизненный цикл модуля ── + +// Подключить таблицы, router-сервис и обработку сохранённых задач; 0/-1. int chat_attachment_init(struct UTUN_INSTANCE* inst); + +// Запретить новые подготовки и остановить таймер; запущенные workers завершаются отдельно. void chat_attachment_quiesce(struct UTUN_INSTANCE* inst); + +// После завершения workers убрать незавершённые подготовки и отключить router-сервис. void chat_attachment_destroy(struct UTUN_INSTANCE* inst); -/* source — собственный подготовленный файл приложения, передаётся во владение. */ + +// ── Отправка и учёт вложений ── + +/* Асинхронно отправить подготовленный файл; channel — десятичный ID, reply — 32 байта ID ответа либо нули. + * Аргументы копируются. 0 — задача принята, source передан задаче и будет удалён даже при ошибке подготовки. + * -1 — отказ запуска, source остаётся у вызывающего. Ненулевой request возвращается в CHAT_EVT_MSG_SUBMITTED. */ int chat_attachment_send(struct UTUN_INSTANCE* inst, const char* channel, const char* source, const struct attachment_info* info, const char* type, const uint8_t reply[32], uint64_t request); -/* UUID из подписанного имени контейнера; принимает канонический base64-prefix тела медиа. */ + +// Прочитать UUID из канонического base64-префикса тела медиа; 0/-1. Подпись проверяет вызывающий. int chat_attachment_identity(const char* body, size_t length, uint8_t id[16]); -/* Связать заранее сохранённый авторский ключ с подписанным сообщением до публикации. */ + +// Связать авторский ключ с ID сообщения и SHA256 контейнера до публикации; 0/-1. Массивы копируются в БД. int chat_attachment_bind(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t id[16], const uint8_t message[32], const uint8_t hash[32]); -/* Учесть проверенное db_sync сообщение; обычные сообщения игнорируются. */ + +// Учесть проверенное db_sync сообщение и сохранить метаданные контейнера; 0/-1. Обычные сообщения игнорируются. int chat_attachment_observe(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], uint64_t author, const char* json, size_t length); -/* После проверенной загрузки: сохранить состояние, запросить ключ либо расшифровать. */ + +// ── Получение файла и выдача доступа ── + +// После проверенной загрузки: сохранить состояние, запросить ключ либо расшифровать. void chat_attachment_downloaded(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]); -/* Ручная загрузка на storage-узле также означает запрос пользовательского доступа. */ + +// Ручная загрузка на storage-узле также означает запрос пользовательского доступа. void chat_attachment_want(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]); + +/* Сохранить решение автора по ожидающему запросу peer: approve!=0 — выдать ключ, 0 — отказать. + * 0 — решение и исходящий ответ записаны в БД, -1 — ошибка/нет прав или ожидающего запроса; доставка идёт позже. */ int chat_attachment_decide(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], uint64_t peer, int approve); + +// ── Команда интерфейса ── + struct chat_attachment_decision { struct UTUN_INSTANCE* inst; char channel[32]; @@ -37,9 +63,14 @@ struct chat_attachment_decision { uint64_t peer; int approve; }; +// arg — u_malloc/u_calloc chat_attachment_decision, освобождается здесь; результат — CHAT_EVT_ATTACHMENT_KEYS_CHANGED. void chat_attachment_decision_trampoline(void* arg); -/* Канал удаляется локально: отменить очереди, стереть ключи и частные копии. */ + +// ── Локальное удаление канала ── + +// Канал удаляется локально: отменить очереди, стереть ключи и частные копии. void chat_attachment_delete_channel(struct UTUN_INSTANCE* inst, const char* channel); + #ifdef __cplusplus } #endif diff --git a/src/chat/chat_attachment.h1 b/src/chat/chat_attachment.h1 new file mode 100644 index 00000000..cd5c4ae2 --- /dev/null +++ b/src/chat/chat_attachment.h1 @@ -0,0 +1,46 @@ +/* Зашифрованные групповые вложения и ручная выдача ключей. + * Все API кроме identity — uasync-поток. Ключи, запросы и outbox локальны. + * init после media_async; quiesce до его destroy, destroy после workers. + * Отдельный router-сервис требует E2E-шифрование и подпись каждого пакета. + * Очереди переживают restart; для доставки нужен живой маршрут обеих сторон. */ +#ifndef CHAT_ATTACHMENT_H +#define CHAT_ATTACHMENT_H +#include "../media_async/attachment.h" +#include "../media_async/encrypted_file.h" +#ifdef __cplusplus +extern "C" { +#endif +struct UTUN_INSTANCE; +int chat_attachment_init(struct UTUN_INSTANCE* inst); +void chat_attachment_quiesce(struct UTUN_INSTANCE* inst); +void chat_attachment_destroy(struct UTUN_INSTANCE* inst); +/* source — собственный подготовленный файл приложения, передаётся во владение. */ +int chat_attachment_send(struct UTUN_INSTANCE* inst, const char* channel, const char* source, + const struct attachment_info* info, const char* type, const uint8_t reply[32], uint64_t request); +/* UUID из подписанного имени контейнера; принимает канонический base64-prefix тела медиа. */ +int chat_attachment_identity(const char* body, size_t length, uint8_t id[16]); +/* Связать заранее сохранённый авторский ключ с подписанным сообщением до публикации. */ +int chat_attachment_bind(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t id[16], + const uint8_t message[32], const uint8_t hash[32]); +/* Учесть проверенное db_sync сообщение; обычные сообщения игнорируются. */ +int chat_attachment_observe(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], + uint64_t author, const char* json, size_t length); +/* После проверенной загрузки: сохранить состояние, запросить ключ либо расшифровать. */ +void chat_attachment_downloaded(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]); +/* Ручная загрузка на storage-узле также означает запрос пользовательского доступа. */ +void chat_attachment_want(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]); +int chat_attachment_decide(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], uint64_t peer, int approve); +struct chat_attachment_decision { + struct UTUN_INSTANCE* inst; + char channel[32]; + uint8_t message[32]; + uint64_t peer; + int approve; +}; +void chat_attachment_decision_trampoline(void* arg); +/* Канал удаляется локально: отменить очереди, стереть ключи и частные копии. */ +void chat_attachment_delete_channel(struct UTUN_INSTANCE* inst, const char* channel); +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/chat/chat_core.h b/src/chat/chat_core.h index 526e7fb1..d6d01b32 100644 --- a/src/chat/chat_core.h +++ b/src/chat/chat_core.h @@ -2,6 +2,8 @@ * chat_core — API каналов, сообщений, профиля и настроек одного узла. * chat_service_start/stop управляют всем чат-сервисом поверх работающего ядра. * Данные хранятся в общей SQLite; события для интерфейса доставляет chat_event. + * Порядок: utun_core_start → chat_service_start → команды чата → chat_service_stop. + * Сообщения отправляются через submit_message или submit_media_message; результат GUI — CHAT_EVT_MSG_SUBMITTED. * * Все DB-операции и сетевые функции выполняются в uasync-потоке. * Все функции per-instance: первым аргументом struct UTUN_INSTANCE*. @@ -19,10 +21,11 @@ struct sqlite3; struct UTUN_INSTANCE; -/* ── Жизненный цикл ── */ +// ── Жизненный цикл ── -/* Запустить чат поверх core_start; UTUN не требуется. Повтор допустим; 0/-1. */ +// Запустить чат поверх core_start; UTUN не требуется. Повтор допустим; 0/-1. int chat_service_start(struct UTUN_INSTANCE* inst); + /* Остановить чат, сохранив ядро, UTUN и БД. Повтор допустим; вызывать вне callbacks чата. * Отменяет доставку и ждёт native media workers; длительная задача может задержать stop. */ void chat_service_stop(struct UTUN_INSTANCE* inst); @@ -30,7 +33,8 @@ void chat_service_stop(struct UTUN_INSTANCE* inst); /* Внутренняя часть запуска: открыть/подключить БД и загрузить каналы. 0/-1. * Для запуска всего чат-сервиса использовать chat_service_start(). */ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path); -/* Освободить chat_core после остановки остальных частей чата; общая БД остаётся у ядра. */ + +// Освободить chat_core после остановки остальных частей чата; общая БД остаётся у ядра. void chat_core_destroy(struct UTUN_INSTANCE* inst); /* Backfill: регистрирует уже скачанные медиафайлы (лежат на диске, но отсутствуют @@ -44,14 +48,16 @@ void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst); /* Регистрирует триггер: после первого SYNC_DONE (+ дебаунс 2с) анонсирует локальные * блоки суперузлам и запускает chat_media_autodownload_backfill. Один раз за запуск чата. */ void chat_media_startup_backfill_init(struct UTUN_INSTANCE* inst); + void chat_media_startup_backfill_destroy(struct UTUN_INSTANCE* inst); -/* Заимствованная БД текущего chat_core; NULL до init/после destroy. */ +// Заимствованная БД текущего chat_core; NULL до init/после destroy. struct sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst); -/* 1 — chat_core инициализирован, 0 — нет; не проверяет доступность сети. */ + +// 1 — chat_core инициализирован, 0 — нет; не проверяет доступность сети. int chat_core_is_initialized(struct UTUN_INSTANCE* inst); -/* ── Отправка сообщения (GUI → uasync) ── */ +// ── Отправка сообщения (GUI → uasync) ── struct chat_msg_submit { struct UTUN_INSTANCE* inst; @@ -72,31 +78,38 @@ struct chat_msg_submit { uint64_t request_id; /* ненулевой ID: trampoline/media отправляет CHAT_EVT_MSG_SUBMITTED */ }; -/* Подписать, сохранить и разослать сообщение. req/data остаются у вызывающего. 0/-1. */ +// Подписать, сохранить и разослать сообщение. req/data остаются у вызывающего. 0/-1. int chat_core_submit_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req); -/* Отправка медиа-сообщения (вложение): src->dest (copy) или видео (transcode). */ +/* Подготовить файл в worker, затем сохранить и разослать сообщение; результат — CHAT_EVT_MSG_SUBMITTED при request_id!=0. + * req и data копируются до возврата и остаются у вызывающего; файлы должны жить до завершения подготовки. + * media_copy копирует src в dest, media_video запускает транскодирование. Ошибка также приходит событием. */ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req); -/* Ручная загрузка вложения; для зашифрованного файла также запрашивает ключ автора. */ + +// Ручная загрузка вложения; для зашифрованного файла также запрашивает ключ автора. void chat_core_attachment_download(struct UTUN_INSTANCE* inst, const char* channel_id, int64_t msg_id); -/* ── DB-операции (оставлены для интроспекции) ── */ +// ── DB-операции (оставлены для интроспекции) ── -/* Число локальных сообщений; 0 также при отсутствии БД или ошибке запроса. */ +// Число локальных сообщений; 0 также при отсутствии БД или ошибке запроса. uint32_t chat_core_count(struct UTUN_INSTANCE* inst, const char* ch_id); + /* Хеш записи по позиции с нуля в порядке (timestamp, node_id); hash_out — 32 байта. * 0 = запрос выполнен (нет записи — нулевой хеш), -1 = ошибка. */ int chat_core_chain_hash_at(struct UTUN_INSTANCE* inst, const char* ch_id, uint32_t pos, uint8_t* hash_out); + /* Список ID: count:u16, затем [длина:u8, строка]; buf_size >= 2. 0/-1. * Списки ограничены буфером; out_len — записанные байты, числа в порядке байтов хоста. */ int chat_core_list_channels(struct UTUN_INSTANCE* inst, uint8_t* buf, size_t buf_size, size_t* out_len); -/* Список участников: count:u16, затем node_id:u64; buf_size >= 2. 0/-1. */ + +// Список участников: count:u16, затем node_id:u64; buf_size >= 2. 0/-1. int chat_core_list_peers(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t* buf, size_t buf_size, size_t* out_len); + /* Ключи X25519/Ed25519 (по 32 байта) и список адресов из БД; buf_size >= 65. * out_len — размер результата; 0 = прочитано, -1 = нет узла или ошибка. */ int chat_core_load_nodeinfo(struct UTUN_INSTANCE* inst, uint64_t node_id, uint8_t* buf, size_t buf_size, size_t* out_len); -/* ── Создание канала (GUI → uasync) ── */ +// ── Создание канала (GUI → uasync) ── struct chat_channel_create { struct UTUN_INSTANCE* inst; @@ -112,16 +125,20 @@ struct chat_channel_create { struct chat_create_auto_arg { struct UTUN_INSTANCE* inst; char name[128]; }; -/* Подготовить локальные таблицы, группу и синхронизацию канала. Членства на других узлах не даёт. */ +// Подготовить локальные таблицы, группу и синхронизацию канала. Членства на других узлах не даёт. void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id); -/* Создать канал по готовым ключам и метаданным req; req остаётся у вызывающего. */ + +// Создать канал по готовым ключам и метаданным req; req остаётся у вызывающего. void chat_core_create_channel(struct UTUN_INSTANCE* inst, struct chat_channel_create* req); + void chat_core_create_channel_trampoline(void* arg); -/* Сгенерировать ключи и создать канал, добавив себя владельцем. */ + +// Сгенерировать ключи и создать канал, добавив себя владельцем. void chat_core_create_channel_auto(struct UTUN_INSTANCE* inst, const char* name); + void chat_core_create_channel_auto_trampoline(void* arg); -/* ── Локальное удаление канала (GUI → uasync) ── */ +// ── Локальное удаление канала (GUI → uasync) ── struct chat_delete_channel_req { struct UTUN_INSTANCE* inst; @@ -132,29 +149,34 @@ struct chat_delete_channel_req { * (соединения/синк), db_sync инстанса, таблиц msg_ и peers_, медиафайлов и * merkle-хешей. Работает в uasync-потоке. Другие узлы канал не теряют. */ void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id); + void chat_core_delete_channel_trampoline(void* arg); -/* ── Утилиты ── */ +// ── Утилиты ── -/* Задать локальный ID в контексте чата; ключи и идентичность ядра не меняет. */ +// Задать локальный ID в контексте чата; ключи и идентичность ядра не меняет. void chat_core_set_my_node_id(struct UTUN_INSTANCE* inst, uint64_t node_id); -/* Сохранить имя узла и обновить собственные member-записи каналов. */ + +// Сохранить имя узла и обновить собственные member-записи каналов. void chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name); struct update_my_name_arg { struct UTUN_INSTANCE* inst; char name[128]; }; void chat_core_update_my_name_trampoline(void* arg); -/* Обновить адреса topo_node и собственные member-записи каналов. */ + +// Обновить адреса topo_node и собственные member-записи каналов. void chat_core_sync_my_addresses(struct UTUN_INSTANCE* inst); + /* При изменении своего userinfo в каналах увеличить update_ts, подписать и запустить sync. * Адреса обновляются отдельно через topo_node; подпись member-блока их не содержит. */ void chat_core_update_my_member(struct UTUN_INSTANCE* inst); -/* Сохранение key-value в ui_state (вызывается из uasync-потока) */ +// Сохранение key-value в ui_state (вызывается из uasync-потока) void chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value); /* Per-channel «play sound on new message» (ui_state: ch__sound_on_message, default 1=on). * set — сохраняет и рассылает пирам через chat_sync (троттлинг db_sync на сервере). */ int chat_core_get_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id); + void chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on); struct chat_sound_arg { struct UTUN_INSTANCE* inst; char ch_id[64]; int on; }; @@ -163,18 +185,20 @@ void chat_core_set_sound_on_message_trampoline(void* arg); struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[1024]; }; void chat_core_save_ui_state_trampoline(void* arg); -/* Установка chat-настройки (name=value) через chat_setting API (GUI → uasync) */ +// Установка chat-настройки (name=value) через chat_setting API (GUI → uasync) void chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value); struct chat_setting_arg { struct UTUN_INSTANCE* inst; char data[256]; }; void chat_core_set_setting_trampoline(void* arg); -/* Запуск подключения к узлам выбранного канала (GUI → uasync) */ +// Запуск подключения к узлам выбранного канала (GUI → uasync) void chat_core_connect_channel(struct UTUN_INSTANCE* inst, const char* ch_id); + void chat_core_connect_channel_trampoline(void* arg); -/* Однократная попытка подключения к конкретному узлу канала (GUI → uasync) */ +// Однократная попытка подключения к конкретному узлу канала (GUI → uasync) void chat_core_connect_node(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id); + void chat_core_connect_node_trampoline(void* arg); struct chat_connect_node_req { @@ -183,10 +207,12 @@ struct chat_connect_node_req { uint64_t node_id; }; -/* Трамплин для gui_bridge_post_uasync (GUI → uasync) */ +/* Передать в uasync блок chat_msg_submit, выделенный u_calloc/u_malloc; trampoline освобождает его. + * data отдельно не освобождается: удобно разместить содержимое в хвосте того же блока. + * Непустой media_src выбирает подготовку вложения, иначе — обычную отправку. Результат — по request_id. */ void chat_core_submit_trampoline(void* arg); -/* Трамплин для ручного запуска загрузки медиа (JNI → uasync) */ +// Трамплин для ручного запуска загрузки медиа (JNI → uasync) struct attachment_dl_req { struct UTUN_INSTANCE* inst; char channel_id[64]; @@ -194,14 +220,15 @@ struct attachment_dl_req { }; void chat_core_attachment_download_trampoline(void* arg); -/* Обновление local_attrs для записи (вызывается из uasync-потока) */ +// Обновление local_attrs для записи (вызывается из uasync-потока) int chat_core_update_local_attrs(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t ts, const uint8_t* author_sig, const char* local_attrs_json); -/* Пометить голосовое сообщение как проигранное (добавляет "pl":1 в local_attrs) */ +// Пометить голосовое сообщение как проигранное (добавляет "pl":1 в local_attrs) void chat_core_mark_voice_played(struct UTUN_INSTANCE* inst, const char* channel_id, uint64_t ts, uint64_t author_node_id); + void chat_core_mark_voice_played_trampoline(void* arg); struct voice_played_req { @@ -211,26 +238,26 @@ struct voice_played_req { uint64_t author_node_id; }; -/* Сбор статуса (NTP + connections) и отправка в GUI. arg = struct UTUN_INSTANCE* (inst) */ +// Сбор статуса (NTP + connections) и отправка в GUI. arg = struct UTUN_INSTANCE* (inst) void chat_core_collect_status_trampoline(void* arg); -/* Лёгкий запрос нагрузки uasync для GUI. arg = inst, ответ CHAT_EVT_CPU_LOAD. */ +// Лёгкий запрос нагрузки uasync для GUI. arg = inst, ответ CHAT_EVT_CPU_LOAD. void chat_core_collect_cpu_load_trampoline(void* arg); -/* Сбор списка ETCP-соединений и отправка в GUI (binary packet). arg = inst */ +// Сбор списка ETCP-соединений и отправка в GUI (binary packet). arg = inst void chat_core_collect_conn_list_trampoline(void* arg); struct chat_node_arg { struct UTUN_INSTANCE* inst; uint64_t node_id; }; -/* Сбор полных метрик одного соединения (arg = u_malloc struct chat_node_arg) */ +// Сбор полных метрик одного соединения (arg = u_malloc struct chat_node_arg) void chat_core_collect_conn_metrics_trampoline(void* arg); -/* Сбор live-снапшота для панели деталей участника (arg = u_malloc struct chat_node_arg) */ +// Сбор live-снапшота для панели деталей участника (arg = u_malloc struct chat_node_arg) void chat_core_collect_member_detail_trampoline(void* arg); -/* ── Headless CLI API ── */ +// ── Headless CLI API ── -/* Список каналов с метаданными в JSON: [{id,name,owner_id,owner_name,peers,msgs,online}] */ +// Список каналов с метаданными в JSON: [{id,name,owner_id,owner_name,peers,msgs,online}] int chat_core_get_channels_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len); /* Сообщения канала в JSON: [{id,ts,author_id,author_name,content_type,data,local_attrs}]. @@ -242,20 +269,24 @@ int chat_core_get_messages_json(struct UTUN_INSTANCE* inst, const char* ch_id, i * connected означает наличие транспорта в реестре, а не его UP или готовность группы. */ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len); -/* Имя узла из таблицы nodes */ +// Имя узла из таблицы nodes int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz); -/* Ссылка на описание avatars/.json внутри дерева своего UID/канала. server=0 удаляет ссылку. */ +// Ссылка на описание avatars/.json внутри дерева своего UID/канала. server=0 удаляет ссылку. int chat_core_set_my_avatar(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t server, const char* file); + int chat_core_name_userinfo(const char* name, char* out, size_t capacity); + int chat_core_get_my_profile(struct UTUN_INSTANCE* inst, const char* channel, char* out, size_t capacity, uint8_t signature[64], uint64_t* timestamp); + /* Одно подписанное обновление нескольких собственных строковых опций. NULL value удаляет ключ. * Остальные поля сохраняются; имя/аватар меняются своими API. Ошибка не меняет БД/Merkle. * Вызов только в uasync-потоке, вне внешней SQLite-транзакции. */ struct chat_profile_option { const char* key; const char* value; }; int chat_core_set_my_profile_options(struct UTUN_INSTANCE* inst, const char* channel, const struct chat_profile_option* options, size_t count); + struct chat_avatar_req { struct UTUN_INSTANCE* inst; uint64_t id, group, server; @@ -263,33 +294,33 @@ struct chat_avatar_req { }; void chat_core_set_my_avatar_trampoline(void* arg); -/* ── Редактирование adm_tags (владелец канала) ── */ +// ── Редактирование adm_tags (владелец канала) ── #define CHAT_MEMBER_TAGS_MAX_KEYS 16 #define CHAT_MEMBER_TAGS_KEY_SZ 64 struct chat_member_tags; -/* Загрузить текущие adm_tags мембера в память. Возвращает NULL если мембера нет. */ +// Загрузить текущие adm_tags мембера в память. Возвращает NULL если мембера нет. struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id); -/* Установить ключ. val=NULL → удалить ключ. ver — особый, автоинкремент при commit. */ +// Установить ключ. val=NULL → удалить ключ. ver — особый, автоинкремент при commit. int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val); -/* Получить значение ключа. Возвращает NULL если ключ не найден. */ +// Получить значение ключа. Возвращает NULL если ключ не найден. const char* chat_member_tags_get(struct chat_member_tags* t, const char* key); -/* Заменить все теги данными из JSON-строки (плоский, строковые значения). */ +// Заменить все теги данными из JSON-строки (плоский, строковые значения). int chat_member_tags_replace(struct chat_member_tags* t, const char* json); /* Сохранить: сериализовать в JSON, инкрементировать ver, подписать канальным Ed25519, * записать через member_sync_put() → синхронизируется всем. */ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* t); -/* Освободить без сохранения. */ +// Освободить без сохранения. void chat_member_tags_free(struct chat_member_tags* t); -/* Трамплин для gui_bridge: arg = struct chat_member_tags_req* (доверенная память). */ +// Трамплин для gui_bridge: arg = struct chat_member_tags_req* (доверенная память). struct chat_member_tags_req { struct UTUN_INSTANCE* inst; char ch_id[64]; @@ -298,10 +329,11 @@ struct chat_member_tags_req { }; void chat_member_tags_commit_trampoline(void* arg); -/* ── Передача прав админа (владелец → другому узлу) ── */ +// ── Передача прав админа (владелец → другому узлу) ── -/* Передать оба канальных privkey выбранному узлу (encrypted+signed через etcp_router). */ +// Передать оба канальных privkey выбранному узлу (encrypted+signed через etcp_router). void chat_core_transfer_admin(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id); + void chat_core_transfer_admin_trampoline(void* arg); struct chat_transfer_admin_req { diff --git a/src/chat/chat_core.h1 b/src/chat/chat_core.h1 new file mode 100644 index 00000000..526e7fb1 --- /dev/null +++ b/src/chat/chat_core.h1 @@ -0,0 +1,313 @@ +/* + * chat_core — API каналов, сообщений, профиля и настроек одного узла. + * chat_service_start/stop управляют всем чат-сервисом поверх работающего ядра. + * Данные хранятся в общей SQLite; события для интерфейса доставляет chat_event. + * + * Все DB-операции и сетевые функции выполняются в uasync-потоке. + * Все функции per-instance: первым аргументом struct UTUN_INSTANCE*. + * GUI вызывает через gui_bridge_post_uasync() trampoline-функции, + * чей arg-структура несёт inst. + */ + +#ifndef CHAT_CORE_H +#define CHAT_CORE_H + +#include +#include +#include + +struct sqlite3; +struct UTUN_INSTANCE; + +/* ── Жизненный цикл ── */ + +/* Запустить чат поверх core_start; UTUN не требуется. Повтор допустим; 0/-1. */ +int chat_service_start(struct UTUN_INSTANCE* inst); +/* Остановить чат, сохранив ядро, UTUN и БД. Повтор допустим; вызывать вне callbacks чата. + * Отменяет доставку и ждёт native media workers; длительная задача может задержать stop. */ +void chat_service_stop(struct UTUN_INSTANCE* inst); + +/* Внутренняя часть запуска: открыть/подключить БД и загрузить каналы. 0/-1. + * Для запуска всего чат-сервиса использовать chat_service_start(). */ +int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path); +/* Освободить chat_core после остановки остальных частей чата; общая БД остаётся у ядра. */ +void chat_core_destroy(struct UTUN_INSTANCE* inst); + +/* Backfill: регистрирует уже скачанные медиафайлы (лежат на диске, но отсутствуют + * в media_files) — вызывается после загрузки каналов/сообщений при старте. */ +void chat_media_backfill(struct UTUN_INSTANCE* inst); + +/* Докачка недостающего медиа за последние storage_backfill_days дней. Проходит по + * медиасообщениям каналов и запускает загрузку для тех, чей файл отсутствует на диске. */ +void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst); + +/* Регистрирует триггер: после первого SYNC_DONE (+ дебаунс 2с) анонсирует локальные + * блоки суперузлам и запускает chat_media_autodownload_backfill. Один раз за запуск чата. */ +void chat_media_startup_backfill_init(struct UTUN_INSTANCE* inst); +void chat_media_startup_backfill_destroy(struct UTUN_INSTANCE* inst); + +/* Заимствованная БД текущего chat_core; NULL до init/после destroy. */ +struct sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst); +/* 1 — chat_core инициализирован, 0 — нет; не проверяет доступность сети. */ +int chat_core_is_initialized(struct UTUN_INSTANCE* inst); + +/* ── Отправка сообщения (GUI → uasync) ── */ + +struct chat_msg_submit { + struct UTUN_INSTANCE* inst; + char channel_id[64]; /* десятичный ID канала */ + char content_type[32]; /* тип содержимого сообщения */ + char media_src[1024]; /* исходный файл вложения */ + char media_dest[1024]; /* путь подготовленного локального файла */ + uint8_t media_copy; /* 1 = скопировать исходный файл при регистрации */ + uint8_t media_video; /* 1 = подготовить как видео (пробинг+транскод) */ + int duration_ms; /* видео: длительность (0 = неизвестно) */ + int width; /* видео: ширина */ + int height; /* видео: высота */ + uint8_t* data; /* содержимое сообщения, заимствовано обычным submit */ + uint32_t data_len; /* размер data в байтах */ + uint64_t timestamp; /* успешная отправка возвращает назначенное время */ + uint8_t message_id[32]; /* успешная обычная отправка возвращает общий ID */ + uint8_t reply_to[32]; /* общий ID оригинала в этом канале; нули = не ответ */ + uint64_t request_id; /* ненулевой ID: trampoline/media отправляет CHAT_EVT_MSG_SUBMITTED */ +}; + +/* Подписать, сохранить и разослать сообщение. req/data остаются у вызывающего. 0/-1. */ +int chat_core_submit_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req); + +/* Отправка медиа-сообщения (вложение): src->dest (copy) или видео (transcode). */ +void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req); +/* Ручная загрузка вложения; для зашифрованного файла также запрашивает ключ автора. */ +void chat_core_attachment_download(struct UTUN_INSTANCE* inst, const char* channel_id, int64_t msg_id); + +/* ── DB-операции (оставлены для интроспекции) ── */ + +/* Число локальных сообщений; 0 также при отсутствии БД или ошибке запроса. */ +uint32_t chat_core_count(struct UTUN_INSTANCE* inst, const char* ch_id); +/* Хеш записи по позиции с нуля в порядке (timestamp, node_id); hash_out — 32 байта. + * 0 = запрос выполнен (нет записи — нулевой хеш), -1 = ошибка. */ +int chat_core_chain_hash_at(struct UTUN_INSTANCE* inst, const char* ch_id, uint32_t pos, uint8_t* hash_out); +/* Список ID: count:u16, затем [длина:u8, строка]; buf_size >= 2. 0/-1. + * Списки ограничены буфером; out_len — записанные байты, числа в порядке байтов хоста. */ +int chat_core_list_channels(struct UTUN_INSTANCE* inst, uint8_t* buf, size_t buf_size, size_t* out_len); +/* Список участников: count:u16, затем node_id:u64; buf_size >= 2. 0/-1. */ +int chat_core_list_peers(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t* buf, size_t buf_size, size_t* out_len); +/* Ключи X25519/Ed25519 (по 32 байта) и список адресов из БД; buf_size >= 65. + * out_len — размер результата; 0 = прочитано, -1 = нет узла или ошибка. */ +int chat_core_load_nodeinfo(struct UTUN_INSTANCE* inst, uint64_t node_id, uint8_t* buf, size_t buf_size, size_t* out_len); + +/* ── Создание канала (GUI → uasync) ── */ + +struct chat_channel_create { + struct UTUN_INSTANCE* inst; + char channel_id[64]; + char name[128]; + uint64_t owner_node_id; + uint8_t x25519_pubkey[32]; + uint8_t x25519_privkey[32]; + uint8_t ed25519_pubkey[32]; + uint8_t ed25519_privkey[32]; + uint8_t signature[64]; +}; + +struct chat_create_auto_arg { struct UTUN_INSTANCE* inst; char name[128]; }; + +/* Подготовить локальные таблицы, группу и синхронизацию канала. Членства на других узлах не даёт. */ +void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id); +/* Создать канал по готовым ключам и метаданным req; req остаётся у вызывающего. */ +void chat_core_create_channel(struct UTUN_INSTANCE* inst, struct chat_channel_create* req); +void chat_core_create_channel_trampoline(void* arg); +/* Сгенерировать ключи и создать канал, добавив себя владельцем. */ +void chat_core_create_channel_auto(struct UTUN_INSTANCE* inst, const char* name); +void chat_core_create_channel_auto_trampoline(void* arg); + +/* ── Локальное удаление канала (GUI → uasync) ── */ + +struct chat_delete_channel_req { + struct UTUN_INSTANCE* inst; + char ch_id[64]; +}; + +/* Удалить канал с этого устройства: отмена медиа-загрузок, удаление группы + * (соединения/синк), db_sync инстанса, таблиц msg_ и peers_, медиафайлов и + * merkle-хешей. Работает в uasync-потоке. Другие узлы канал не теряют. */ +void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id); +void chat_core_delete_channel_trampoline(void* arg); + +/* ── Утилиты ── */ + +/* Задать локальный ID в контексте чата; ключи и идентичность ядра не меняет. */ +void chat_core_set_my_node_id(struct UTUN_INSTANCE* inst, uint64_t node_id); +/* Сохранить имя узла и обновить собственные member-записи каналов. */ +void chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name); + +struct update_my_name_arg { struct UTUN_INSTANCE* inst; char name[128]; }; +void chat_core_update_my_name_trampoline(void* arg); +/* Обновить адреса topo_node и собственные member-записи каналов. */ +void chat_core_sync_my_addresses(struct UTUN_INSTANCE* inst); +/* При изменении своего userinfo в каналах увеличить update_ts, подписать и запустить sync. + * Адреса обновляются отдельно через topo_node; подпись member-блока их не содержит. */ +void chat_core_update_my_member(struct UTUN_INSTANCE* inst); + +/* Сохранение key-value в ui_state (вызывается из uasync-потока) */ +void chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value); + +/* Per-channel «play sound on new message» (ui_state: ch__sound_on_message, default 1=on). + * set — сохраняет и рассылает пирам через chat_sync (троттлинг db_sync на сервере). */ +int chat_core_get_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id); +void chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on); + +struct chat_sound_arg { struct UTUN_INSTANCE* inst; char ch_id[64]; int on; }; +void chat_core_set_sound_on_message_trampoline(void* arg); + +struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[1024]; }; +void chat_core_save_ui_state_trampoline(void* arg); + +/* Установка chat-настройки (name=value) через chat_setting API (GUI → uasync) */ +void chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value); + +struct chat_setting_arg { struct UTUN_INSTANCE* inst; char data[256]; }; +void chat_core_set_setting_trampoline(void* arg); + +/* Запуск подключения к узлам выбранного канала (GUI → uasync) */ +void chat_core_connect_channel(struct UTUN_INSTANCE* inst, const char* ch_id); +void chat_core_connect_channel_trampoline(void* arg); + +/* Однократная попытка подключения к конкретному узлу канала (GUI → uasync) */ +void chat_core_connect_node(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id); +void chat_core_connect_node_trampoline(void* arg); + +struct chat_connect_node_req { + struct UTUN_INSTANCE* inst; + char ch_id[64]; + uint64_t node_id; +}; + +/* Трамплин для gui_bridge_post_uasync (GUI → uasync) */ +void chat_core_submit_trampoline(void* arg); + +/* Трамплин для ручного запуска загрузки медиа (JNI → uasync) */ +struct attachment_dl_req { + struct UTUN_INSTANCE* inst; + char channel_id[64]; + int64_t msg_id; +}; +void chat_core_attachment_download_trampoline(void* arg); + +/* Обновление local_attrs для записи (вызывается из uasync-потока) */ +int chat_core_update_local_attrs(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t ts, + const uint8_t* author_sig, + const char* local_attrs_json); + +/* Пометить голосовое сообщение как проигранное (добавляет "pl":1 в local_attrs) */ +void chat_core_mark_voice_played(struct UTUN_INSTANCE* inst, const char* channel_id, uint64_t ts, + uint64_t author_node_id); +void chat_core_mark_voice_played_trampoline(void* arg); + +struct voice_played_req { + struct UTUN_INSTANCE* inst; + char channel_id[64]; + uint64_t ts; + uint64_t author_node_id; +}; + +/* Сбор статуса (NTP + connections) и отправка в GUI. arg = struct UTUN_INSTANCE* (inst) */ +void chat_core_collect_status_trampoline(void* arg); + +/* Лёгкий запрос нагрузки uasync для GUI. arg = inst, ответ CHAT_EVT_CPU_LOAD. */ +void chat_core_collect_cpu_load_trampoline(void* arg); + +/* Сбор списка ETCP-соединений и отправка в GUI (binary packet). arg = inst */ +void chat_core_collect_conn_list_trampoline(void* arg); + +struct chat_node_arg { struct UTUN_INSTANCE* inst; uint64_t node_id; }; + +/* Сбор полных метрик одного соединения (arg = u_malloc struct chat_node_arg) */ +void chat_core_collect_conn_metrics_trampoline(void* arg); + +/* Сбор live-снапшота для панели деталей участника (arg = u_malloc struct chat_node_arg) */ +void chat_core_collect_member_detail_trampoline(void* arg); + +/* ── Headless CLI API ── */ + +/* Список каналов с метаданными в JSON: [{id,name,owner_id,owner_name,peers,msgs,online}] */ +int chat_core_get_channels_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len); + +/* Сообщения канала в JSON: [{id,ts,author_id,author_name,content_type,data,local_attrs}]. + * От новых к старым; count<=0 — до 100 записей, offset — сколько пропустить. */ +int chat_core_get_messages_json(struct UTUN_INSTANCE* inst, const char* ch_id, int count, int offset, + char* buf, size_t buf_size, size_t* out_len); + +/* Мемберы в JSON: [{node_id,name,online,connected,adm_tags,x25519,ed25519,addrs}]. + * connected означает наличие транспорта в реестре, а не его UP или готовность группы. */ +int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len); + +/* Имя узла из таблицы nodes */ +int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz); + +/* Ссылка на описание avatars/.json внутри дерева своего UID/канала. server=0 удаляет ссылку. */ +int chat_core_set_my_avatar(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t server, const char* file); +int chat_core_name_userinfo(const char* name, char* out, size_t capacity); +int chat_core_get_my_profile(struct UTUN_INSTANCE* inst, const char* channel, char* out, size_t capacity, + uint8_t signature[64], uint64_t* timestamp); +/* Одно подписанное обновление нескольких собственных строковых опций. NULL value удаляет ключ. + * Остальные поля сохраняются; имя/аватар меняются своими API. Ошибка не меняет БД/Merkle. + * Вызов только в uasync-потоке, вне внешней SQLite-транзакции. */ +struct chat_profile_option { const char* key; const char* value; }; +int chat_core_set_my_profile_options(struct UTUN_INSTANCE* inst, const char* channel, + const struct chat_profile_option* options, size_t count); +struct chat_avatar_req { + struct UTUN_INSTANCE* inst; + uint64_t id, group, server; + char file[70]; +}; +void chat_core_set_my_avatar_trampoline(void* arg); + +/* ── Редактирование adm_tags (владелец канала) ── */ + +#define CHAT_MEMBER_TAGS_MAX_KEYS 16 +#define CHAT_MEMBER_TAGS_KEY_SZ 64 + +struct chat_member_tags; + +/* Загрузить текущие adm_tags мембера в память. Возвращает NULL если мембера нет. */ +struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id); + +/* Установить ключ. val=NULL → удалить ключ. ver — особый, автоинкремент при commit. */ +int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val); + +/* Получить значение ключа. Возвращает NULL если ключ не найден. */ +const char* chat_member_tags_get(struct chat_member_tags* t, const char* key); + +/* Заменить все теги данными из JSON-строки (плоский, строковые значения). */ +int chat_member_tags_replace(struct chat_member_tags* t, const char* json); + +/* Сохранить: сериализовать в JSON, инкрементировать ver, подписать канальным Ed25519, + * записать через member_sync_put() → синхронизируется всем. */ +int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* t); + +/* Освободить без сохранения. */ +void chat_member_tags_free(struct chat_member_tags* t); + +/* Трамплин для gui_bridge: arg = struct chat_member_tags_req* (доверенная память). */ +struct chat_member_tags_req { + struct UTUN_INSTANCE* inst; + char ch_id[64]; + uint64_t node_id; + char json[512]; +}; +void chat_member_tags_commit_trampoline(void* arg); + +/* ── Передача прав админа (владелец → другому узлу) ── */ + +/* Передать оба канальных privkey выбранному узлу (encrypted+signed через etcp_router). */ +void chat_core_transfer_admin(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id); +void chat_core_transfer_admin_trampoline(void* arg); + +struct chat_transfer_admin_req { + struct UTUN_INSTANCE* inst; + char ch_id[64]; + uint64_t target_node_id; +}; + +#endif /* CHAT_CORE_H */ diff --git a/src/chat/chat_event.h b/src/chat/chat_event.h index cbf59981..66fc4785 100644 --- a/src/chat/chat_event.h +++ b/src/chat/chat_event.h @@ -22,7 +22,7 @@ struct UTUN_INSTANCE; extern "C" { #endif -/* Типы событий (совпадают с GUI_EVT_* в gui_bridge.h для прямой совместимости) */ +// Типы событий (совпадают с GUI_EVT_* в gui_bridge.h для прямой совместимости) #define CHAT_EVT_MSG_RECEIVED 1 /* [ch_id_len:1][ch_id:var][author_node_id:8][initial_sync:1] */ #define CHAT_EVT_CONNECT_RESULT 2 /* [node_id:8][result:4][channel_id:8] */ #define CHAT_EVT_NEW_PEER 3 /* [node_id:8] */ @@ -92,15 +92,22 @@ extern "C" { #define CHAT_EVT_RADIO_CALL 65 /* [id:8LE][group:8LE][peer:8LE][state:1][reason:1][incoming:1], radio_call.h */ #define CHAT_EVT_CALL_PHONE_BUSY 66 /* [id:8LE][reserved:1]; вызывающий выбирает продолжить либо завершить */ +// ── Подключение получателя событий ── + typedef void (*chat_event_handler_fn)(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len); -/* Заменить обработчик; NULL отключает доставку. Предыдущий обработчик не вызывается. */ +// Заменить обработчик; NULL отключает доставку. Предыдущий обработчик не вызывается. void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn handler); -/* Синхронно передать type и len байт data; память остаётся у отправителя. Формат зависит от CHAT_EVT_*. */ + +// ── Отправка событий из ядра ── + +// Синхронно передать type и len байт data; память остаётся у отправителя. Формат зависит от CHAT_EVT_*. void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len); -/* Сообщить этап подготовки вложения: state=0 начало, 1 готово, 2 ошибка; target — ID канала или DM. */ + +// Сообщить этап подготовки вложения: state=0 начало, 1 готово, 2 ошибка; target — ID канала или DM. void chat_event_attachment(struct UTUN_INSTANCE* inst, int is_dm, const char* target, const uint8_t id[16], int state); -/* request_id=0 отключает подтверждение; вызывается один раз после сохранения либо ошибки подготовки. */ + +// request_id=0 отключает подтверждение; вызывается один раз после сохранения либо ошибки подготовки. void chat_event_submitted(struct UTUN_INSTANCE* inst, uint64_t request_id, int success); #ifdef __cplusplus diff --git a/src/chat/chat_event.h1 b/src/chat/chat_event.h1 new file mode 100644 index 00000000..143b58cf --- /dev/null +++ b/src/chat/chat_event.h1 @@ -0,0 +1,104 @@ +/* + * chat_event.h — система нотификаций о событиях чата + * + * Производители (chat_core, chat_sync, ...) вызывают chat_event_post(). + * Потребители (chatgui, headless, будущий HTTP API) регистрируют один обработчик. + * + * Без обработчика — данные логируются через DEBUG_DEBUG + log_dump. + * Все вызовы — из uasync-потока. + */ + +#ifndef CHAT_EVENT_H +#define CHAT_EVENT_H + +#include +#include + +struct UTUN_INSTANCE; + +#ifdef __cplusplus +extern "C" { +#endif + +/* Типы событий (совпадают с GUI_EVT_* в gui_bridge.h для прямой совместимости) */ +#define CHAT_EVT_MSG_RECEIVED 1 /* [ch_id_len:1][ch_id:var][author_node_id:8][initial_sync:1] */ +#define CHAT_EVT_CONNECT_RESULT 2 /* [node_id:8][result:4][channel_id:8] */ +#define CHAT_EVT_NEW_PEER 3 /* [node_id:8] */ +#define CHAT_EVT_CHANNEL_UPDATED 4 /* [ch_id_len:1][ch_id:var] */ +#define CHAT_EVT_MEMBERS_CHANGED 5 /* [ch_id_len:1][ch_id:var] */ +#define CHAT_EVT_MY_NODE_ID 6 /* [node_id:8] */ +#define CHAT_EVT_AUTO_CONNECT_STATUS 7 /* [status:1][total_tried:2][node_count:2][success:2] */ +#define CHAT_EVT_CHANNEL_PEERS_ONLINE 8 /* [ch_id_len:1][ch_id:var][online_count:2] */ +#define CHAT_EVT_DB_READY 9 /* data: none */ +#define CHAT_EVT_STATUS_REFRESH 10 /* data: status text */ +#define CHAT_EVT_KEYS_GENERATED 11 /* [pub_hex:64] */ +#define CHAT_EVT_SERVICE_STARTED 12 /* data: none */ +#define CHAT_EVT_SERVICE_STOPPED 13 /* data: none */ +#define CHAT_EVT_ATTACHMENT_DOWNLOADED 14 /* [ch_id_len:1][ch_id:var][msg_id:8] */ +#define CHAT_EVT_DOWNLOAD_PROGRESS 15 /* [ch_id_len:1][ch_id:var][msg_id:8][blocks_done:4][num_blocks:4] */ +#define CHAT_EVT_NODEINFO_UPDATED 16 /* [node_id:8][conn_presence:1][conn_up:1][best_rtt:2][bgp_rtt:2][bgp_nodes:2] */ +#define CHAT_EVT_LINK_STATUS_CHANGED 17 /* [peer_id:8][link_id:1][state:1][status:1][links_up:1][initialized:1] */ +#define CHAT_EVT_INVITE_RECEIVED 18 /* [ch_id_len:1][ch_id][name_len:1][name][inviter_node_id:8][inviter_name_len:1][inviter_name] */ +#define CHAT_EVT_LOCAL_SOCKETS 19 /* JSON: [{"si":id,"nm":"...","ad":"...","po":port,"pt":"TCP|UDP","st":0|1|2,"lk":n},...] */ +#define CHAT_EVT_CONN_LIST 20 /* [count:2][entry:46B]* — список ETCP-соединений */ +#define CHAT_EVT_CONN_METRICS 21 /* текст — полный дамп метрик выбранного соединения */ +#define CHAT_EVT_MEMBER_DETAIL 22 /* [node_id:8][flags:1][own_tcp_active:2][links...][socks...] — live-снапшот для панели деталей участника */ +#define CHAT_EVT_MEMBER_LIST 23 /* [ch_id_len:1][ch_id:var][count:2][member:CHAT_MEMBER_DISPLAY_SIZE]* */ +#define CHAT_EVT_MEMBER_UPDATED 24 /* [ch_id_len:1][ch_id:var][count:1][member:CHAT_MEMBER_DISPLAY_SIZE] */ +#define CHAT_EVT_MEMBER_REMOVED 25 /* [ch_id_len:1][ch_id:var][node_id:8] — мембер удалён из канала */ +#define CHAT_EVT_BGP_INFO 26 /* текст: "rtt:XXms hop:X via bgp: [XXXX] name1 [XXXX] name2" */ +#define CHAT_EVT_CONNECTING_NODES 27 /* [ch_id_len:1][ch_id:var][count:2][node_id:8]* */ +#define CHAT_EVT_ADMIN_KEY_RECEIVED 28 /* [ch_id_len:1][ch_id:var][from_node_id:8] — получен канальный ключ (стал совладельцем) */ +#define CHAT_EVT_INVITE_LINK_READY 29 /* [ch_id_len:1][ch_id:var][request_id:8][link_len:2][link:var] — пустой link = ошибка */ +#define CHAT_EVT_INVITE_CANDIDATES 30 /* [ch_id_len:1][ch_id:var][auto_node_id:8][count:2][node_id:8]* — достижимые узлы для invite */ +#define CHAT_EVT_CHANNEL_DELETED 31 /* [ch_id_len:1][ch_id:var] — канал удалён локально */ +#define CHAT_EVT_DM_MSG_RECEIVED 32 /* [conv_id_len:1][conv_id:var][author_node_id:8] */ +#define CHAT_EVT_DM_CONV_UPDATED 33 /* [conv_id_len:1][conv_id:var][generation:8LE] — беседа создана/изменена */ +#define CHAT_EVT_CALL_INCOMING 34 /* [ch_id_len:1][ch_id:var][caller_node_id:8][call_id:8] */ +#define CHAT_EVT_CALL_RINGING 35 /* [call_id:8] */ +#define CHAT_EVT_CALL_ACCEPTED 36 /* [call_id:8] */ +#define CHAT_EVT_CALL_ENDED 37 /* [call_id:8][reason:1] */ +#define CHAT_EVT_CALL_DECLINED 38 /* [call_id:8][reason:1] */ +#define CHAT_EVT_CALL_STATS 39 /* [call_id:8][rtt:2][buffer:2][tempo:2][dropped:2][underruns:2][min:2][max:2][reserve:2] */ +#define CHAT_EVT_CALL_ERROR 40 /* [call_id:8][err:1][text:var] */ +#define CHAT_EVT_DM_MESSAGES 41 /* [conv_id_len:1][conv_id:var][JSON:utf8] — сообщения беседы и состояния доставки */ +#define CHAT_EVT_CALL_PATH 42 /* [call_id:8][text:var] — BGP-маршрут до пира звонка */ +#define CHAT_EVT_RADIO_TALK 43 /* [group_id:8][src_node_id:8][stream_id:2][on:1] — кто-то начал/закончил говорить */ +#define CHAT_EVT_CALL_CONNECTION 44 /* [call_id:8][CONN_TYPE:1][next_hop_node_id:8] — текущий путь отправки звонка */ +#define CHAT_EVT_DM_CONVERSATIONS 45 /* [JSON:utf8] — последние 200 личных бесед */ +#define CHAT_EVT_ATTACHMENT_PREPARATION 46 /* [target_len:1][target:var][job_uuid:16][state:1], 0=start 1=ready 2=failed */ + +#define CHAT_EVT_DM_CALL_ROUTE 47 /* [conv_id_len:1][conv_id:var][peer_node_id:8][group_id:8], 0 = нет BGP-пути */ +#define CHAT_EVT_DM_INVITE_RESULT 48 /* [request_id:8][result:4][peer_node_id:8][conv_id:8][generation:8], dm_invite_result */ +#define CHAT_EVT_MEMBER_FILES_RESPONSE 49 /* [group:8LE][server:8LE][mf wire response] */ +#define CHAT_EVT_MEMBER_FILES_TRANSFER 50 /* id/group/server/owner/done/total:8LE,status:2LE,state:1 */ +#define CHAT_EVT_AVATAR_RESULT 51 /* request_id/group:8LE, failed:1; результат публикации ссылки профиля */ +#define CHAT_EVT_GROUP_AVATAR_RESULT 52 /* request_id/group:8LE, failed:1; результат публикации аватарки группы */ +#define CHAT_EVT_FEEDBACK_CHANGED 53 /* [channel_len:1][channel][message_id:32][kind:1] — состояние после commit */ +#define CHAT_EVT_FEEDBACK_RESULT 54 /* [request_id:8LE][success:1][JSON] — результат команды интерфейса */ +#define CHAT_EVT_RADIO_MUTE_RESULT 55 /* [group_id:8LE][node_id:8LE][success:1] — локальный переключатель рации */ +#define CHAT_EVT_MSG_SUBMITTED 56 /* [request_id:8LE][success:1] — результат локального сохранения сообщения */ +#define CHAT_EVT_CPU_LOAD 57 /* [valid:1][average_percent:1] — CPU потока uasync, среднее за секунду, 0..100 */ +#define CHAT_EVT_STREAM_SOURCES 58 /* [group_id:8BE][valid:1][JSON:utf8] — доступные публикации выбранного канала */ +#define CHAT_EVT_DM_DELETE_RESULT 59 /* [request:8LE][conv:8LE][generation:8LE][success:1] — после транзакции */ +#define CHAT_EVT_ATTACHMENT_KEYS_CHANGED 60 /* [channel_len:1][channel][success?:1], без секретов; success — результат команды GUI */ + +#define CHAT_EVT_LOCAL_DEVICES 61 /* [count:2BE][node:8BE,name_len:1,name:UTF8]* */ +#define CHAT_EVT_LOCAL_INVITE 62 /* [node:8BE][name_len:1][name:UTF8][invite_link:UTF8], без NUL */ +#define CHAT_EVT_LOCAL_DELIVERY 63 /* [request:8BE][node:8BE][status:1], local_delivery_status, не результат JOIN */ +#define CHAT_EVT_LOCAL_DISCOVERY_ACTIVE 64 /* [active:1], Android MulticastLock */ +#define CHAT_EVT_RADIO_CALL 65 /* [id:8LE][group:8LE][peer:8LE][state:1][reason:1][incoming:1], radio_call.h */ +#define CHAT_EVT_CALL_PHONE_BUSY 66 /* [id:8LE][reserved:1]; вызывающий выбирает продолжить либо завершить */ + +typedef void (*chat_event_handler_fn)(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len); + +void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn handler); +void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len); +void chat_event_attachment(struct UTUN_INSTANCE* inst, int is_dm, const char* target, const uint8_t id[16], int state); +/* request_id=0 отключает подтверждение; вызывается один раз после сохранения либо ошибки подготовки. */ +void chat_event_submitted(struct UTUN_INSTANCE* inst, uint64_t request_id, int success); + +#ifdef __cplusplus +} +#endif +#endif /* CHAT_EVENT_H */ diff --git a/src/chat/group_avatar.h b/src/chat/group_avatar.h index 5237c4d0..e9bce3fa 100644 --- a/src/chat/group_avatar.h +++ b/src/chat/group_avatar.h @@ -1,32 +1,58 @@ -/* Подписанная аватарка канала: SQLite, максимум 16 КиБ, router-блоки до 4 КиБ. - * Все API/callbacks — uasync; данные публикуются только после полной проверки подписи. */ +/* group_avatar — общая подписанная аватарка CHAT-канала, хранение в SQLite и обмен между участниками. + * После init: get читает текущую версию, set публикует картинку владельца, apply принимает проверяемую запись пира. + * Картинка до 16 КиБ передаётся router-блоками до 4 КиБ; данные публикуются после проверки подписи канала. + * Все вызовы/callbacks — uasync. Изменение картинки приходит событием CHAT_EVT_CHANNEL_UPDATED. */ #ifndef GROUP_AVATAR_H #define GROUP_AVATAR_H #include #include struct UTUN_INSTANCE; struct sqlite3; + +// ── Схема хранения ── + +// Создать поля хранения в общей SQLite; 0/-1, БД остаётся у вызывающего. int group_avatar_schema_init(struct sqlite3* db); + #define GROUP_AVATAR_MAX 16384 #define GROUP_AVATAR_BLOCK 4096 struct group_avatar_record { - uint64_t version; - uint16_t size; - uint8_t hash[32], signature[64], data[GROUP_AVATAR_MAX]; + uint64_t version; // 0 — картинка ещё не публиковалась; иначе версия подписанной записи + uint16_t size; // байты картинки; size=0 при ненулевой версии — подписанное удаление + uint8_t hash[32], signature[64], data[GROUP_AVATAR_MAX]; // SHA256, подпись канала Ed25519 и готовая картинка }; +// ── Жизненный цикл модуля ── + +// Создать контекст и подключить сетевой обмен; 0/-1. Повтор допустим. int group_avatar_init(struct UTUN_INSTANCE* inst); + +// Остановить обмен и освободить контекст; сохранённая картинка остаётся в БД. void group_avatar_destroy(struct UTUN_INSTANCE* inst); + +// Отменить сетевые задачи канала перед удалением группы. void group_avatar_cancel_group(struct UTUN_INSTANCE* inst, uint64_t group); + +// ── Чтение и изменение аватарки ── + +// Скопировать запись и проверить её подпись; 0 — прочитано (version=0, если не публиковалась), -1 — ошибка/нет канала. int group_avatar_get(struct UTUN_INSTANCE* inst, uint64_t group, struct group_avatar_record* record); -/* 1=изменено, 0=прежняя/старая запись; <0 — ошибка. */ + +// Проверить подпись и сохранить более новую запись; 1=изменено, 0=прежняя/старая, -1=ошибка. Структура остаётся у вызывающего. int group_avatar_apply(struct UTUN_INSTANCE* inst, uint64_t group, const struct group_avatar_record* record); -/* Публикация своей картинки требует приватный Ed25519 ключ канала. size=0 — подписанное удаление. */ + +/* Подписать и сохранить картинку: нужен приватный Ed25519 ключ канала. size=0 — удаление; data копируется. + * 0 — сохранено или картинка прежняя, -1 — ошибка/нет прав. Доставка пирам выполняется отдельно. */ int group_avatar_set(struct UTUN_INSTANCE* inst, uint64_t group, const uint8_t* data, size_t size); + +// ── Команда интерфейса ── + struct group_avatar_gui_request { struct UTUN_INSTANCE* inst; uint64_t id, group; uint16_t size; uint8_t data[GROUP_AVATAR_MAX]; }; +// arg — u_malloc/u_calloc group_avatar_gui_request, освобождается здесь; результат — CHAT_EVT_GROUP_AVATAR_RESULT. void group_avatar_gui_set(void* arg); + #endif diff --git a/src/chat/group_avatar.h1 b/src/chat/group_avatar.h1 new file mode 100644 index 00000000..5237c4d0 --- /dev/null +++ b/src/chat/group_avatar.h1 @@ -0,0 +1,32 @@ +/* Подписанная аватарка канала: SQLite, максимум 16 КиБ, router-блоки до 4 КиБ. + * Все API/callbacks — uasync; данные публикуются только после полной проверки подписи. */ +#ifndef GROUP_AVATAR_H +#define GROUP_AVATAR_H +#include +#include +struct UTUN_INSTANCE; +struct sqlite3; +int group_avatar_schema_init(struct sqlite3* db); +#define GROUP_AVATAR_MAX 16384 +#define GROUP_AVATAR_BLOCK 4096 +struct group_avatar_record { + uint64_t version; + uint16_t size; + uint8_t hash[32], signature[64], data[GROUP_AVATAR_MAX]; +}; +int group_avatar_init(struct UTUN_INSTANCE* inst); +void group_avatar_destroy(struct UTUN_INSTANCE* inst); +void group_avatar_cancel_group(struct UTUN_INSTANCE* inst, uint64_t group); +int group_avatar_get(struct UTUN_INSTANCE* inst, uint64_t group, struct group_avatar_record* record); +/* 1=изменено, 0=прежняя/старая запись; <0 — ошибка. */ +int group_avatar_apply(struct UTUN_INSTANCE* inst, uint64_t group, const struct group_avatar_record* record); +/* Публикация своей картинки требует приватный Ed25519 ключ канала. size=0 — подписанное удаление. */ +int group_avatar_set(struct UTUN_INSTANCE* inst, uint64_t group, const uint8_t* data, size_t size); +struct group_avatar_gui_request { + struct UTUN_INSTANCE* inst; + uint64_t id, group; + uint16_t size; + uint8_t data[GROUP_AVATAR_MAX]; +}; +void group_avatar_gui_set(void* arg); +#endif diff --git a/src/media_delivery/media_delivery.h b/src/media_delivery/media_delivery.h index 665c1f62..28b39fad 100644 --- a/src/media_delivery/media_delivery.h +++ b/src/media_delivery/media_delivery.h @@ -1,8 +1,8 @@ -/* media_delivery — канальные медиафайлы: поиск держателей блоков, выдача/relay и репликация доступности. - * Суперузлы обмениваются block_availability, держатели отдают подписанные блоки, media_download собирает файл. - * init создаёт per-instance состояние, bind подключает протокол к router; destroy отменяет сетевую работу. - * Все операции — uasync-поток; долгие файловые/crypto-задачи выполняет media_async. - * E2E-файлы личных бесед используют dm_media/file_transfer, а не канальную таблицу доступности. */ +/* media_delivery — поиск держателей и доставка блоков файлов CHAT-канала, включая промежуточную раздачу (relay). + * После регистрации файла через media_index автор вызывает announce_media; получатель загружает его через media_download. + * Суперузлы обмениваются сведениями о наличии блоков; держатели выдают подписанные блоки, загрузчик собирает файл. + * Чат-сервис вызывает init и затем destroy; bind — другое имя init, вызывать оба последовательно нельзя. + * Все операции — uasync; долгие файловые/crypto-задачи выполняет media_async. Для файлов DM используется dm_media/file_transfer. */ #ifndef MEDIA_DELIVERY_H #define MEDIA_DELIVERY_H @@ -16,7 +16,7 @@ extern "C" { struct UTUN_INSTANCE; -/* ── константы ── */ +// ── константы ── #define MEDIA_MAX_REPL_INFLIGHT 4 #define MEDIA_REPL_TIMEOUT_TB 20000 // начальный таймаут 2s (0.1ms) @@ -29,7 +29,7 @@ struct UTUN_INSTANCE; #define MEDIA_DL_STALL_TIMEOUT_TB 20000 // таймаут простоя загрузки 2s (0.1ms) #define MEDIA_DL_MAX_ATTEMPTS 3 // попыток на блок до ошибки -/* ── структуры ── */ +// ── структуры ── struct media_super_peer { struct ll_entry ll; // индекс по peer_node_id (8 байт) @@ -45,7 +45,7 @@ struct media_super_peer { struct media_delivery_ctx* md; // обратная ссылка }; -/* per-file download tracking (source node admission control) */ +// per-file download tracking (source node admission control) struct md_file_load { struct ll_entry ll; // индекс по media_id (16 байт) @@ -132,12 +132,27 @@ struct media_delivery_ctx { struct UTUN_INSTANCE* inst; }; +// ── Жизненный цикл и роль узла ── + +// Создать таблицы/контекст и подключить router и подписки; 0/-1. Общая БД и группы должны быть готовы. int media_delivery_init(struct UTUN_INSTANCE* inst); + +// Отменить загрузки/выдачи/репликацию и снять подписки; вызывать до удаления групп и общей БД. void media_delivery_destroy(struct UTUN_INSTANCE* inst); + +// Вызывает init целиком; использовать вместо init, а не как следующий этап. 0/-1. int media_delivery_bind(struct UTUN_INSTANCE* inst); + +// Включить/выключить локальную работу суперузла и обмен доступностью блоков. void media_delivery_set_supernode(struct UTUN_INSTANCE* inst, int is_supernode); + +// ── Внутренний учёт выдач ── + +// Внутреннее завершение исходящей выдачи: снять единицу active_streams и отметить завершение для диагностики. void media_delivery_stream_done(struct UTUN_INSTANCE* inst); +// ── Анонсы доступности блоков ── + /* Автор анонсирует свои блоки суперузлам CHAT-группы (HAVE_BLOCK), чтобы первый * скачивающий мог найти держателя через QUERY. Вызывается после регистрации медиа. */ void media_delivery_announce_media(struct UTUN_INSTANCE* inst, uint64_t group_id, @@ -154,19 +169,25 @@ void media_delivery_send_have_block(struct UTUN_INSTANCE* inst, uint64_t group_i * block_availability для уже скачанных ранее медиа. Идемпотентно на суперузле. */ void media_delivery_announce_local_blocks(struct UTUN_INSTANCE* inst); +// ── Отмена работы канала ── + /* Отменить всё активное, что связано с каналом (локальное удаление канала): * загрузки с group_id, их relay-контексты и super_peers этой группы. */ void media_delivery_cancel_channel(struct UTUN_INSTANCE* inst, uint64_t group_id); -/* relay block context helpers (used by media_download.c) */ +// ── Внутренние операции загрузчика: relay ── struct relay_block_ctx* md_relay_find(struct media_delivery_ctx* md, const uint8_t* block_id); + struct relay_block_ctx* md_relay_add(struct media_delivery_ctx* md, const uint8_t* block_id, const uint8_t* media_id, uint32_t chunk); + void md_relay_remove(struct media_delivery_ctx* md, const uint8_t* block_id); -/* file_load helpers (per-file source download tracking) */ +// ── Внутренний учёт нагрузки на файл ── struct md_file_load* md_file_load_find(struct media_delivery_ctx* md, const uint8_t* media_id); + int md_file_load_inc(struct media_delivery_ctx* md, const uint8_t* media_id, uint64_t node_id); + void md_file_load_dec(struct media_delivery_ctx* md, const uint8_t* media_id, uint64_t node_id); #ifdef __cplusplus diff --git a/src/media_delivery/media_delivery.h1 b/src/media_delivery/media_delivery.h1 new file mode 100644 index 00000000..665c1f62 --- /dev/null +++ b/src/media_delivery/media_delivery.h1 @@ -0,0 +1,175 @@ +/* media_delivery — канальные медиафайлы: поиск держателей блоков, выдача/relay и репликация доступности. + * Суперузлы обмениваются block_availability, держатели отдают подписанные блоки, media_download собирает файл. + * init создаёт per-instance состояние, bind подключает протокол к router; destroy отменяет сетевую работу. + * Все операции — uasync-поток; долгие файловые/crypto-задачи выполняет media_async. + * E2E-файлы личных бесед используют dm_media/file_transfer, а не канальную таблицу доступности. */ +#ifndef MEDIA_DELIVERY_H +#define MEDIA_DELIVERY_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include "../lib/ll_queue.h" +#include "../lib/sqlite3.h" + +struct UTUN_INSTANCE; + +/* ── константы ── */ + +#define MEDIA_MAX_REPL_INFLIGHT 4 +#define MEDIA_REPL_TIMEOUT_TB 20000 // начальный таймаут 2s (0.1ms) +#define MEDIA_REPL_TIMEOUT_MAX_TB 6000000 // макс 10 min +#define MEDIA_MAX_DOWNLOADS 10 +#define MEDIA_QUERY_TIMEOUT_TB 20000 // таймаут QUERY 2s +#define MEDIA_HAVE_BLOCK_TIMEOUT_TB 20000 // таймаут HAVE_BLOCK_ACK 2s +#define MEDIA_RECONNECT_COOLDOWN_TB 360000000 // 1 час (0.1ms) +#define MEDIA_HELLO_TIMEOUT_TB 20000 // таймаут SUPER_HELLO 2s +#define MEDIA_DL_STALL_TIMEOUT_TB 20000 // таймаут простоя загрузки 2s (0.1ms) +#define MEDIA_DL_MAX_ATTEMPTS 3 // попыток на блок до ошибки + +/* ── структуры ── */ + +struct media_super_peer { + struct ll_entry ll; // индекс по peer_node_id (8 байт) + uint64_t peer_node_id; + uint64_t group_id; // CHAT-группа, в которой установлена связь с пиром + uint64_t peer_last_recv_id; // что пир говорит он получил от нас (из SUPER_HELLO / SUPER_ACK) + uint32_t timeout_tb; // текущий таймаут (растёт при ошибках) + uint8_t inflight_count; // пакетов в полёте (макс 4) + uint8_t connected; // 1 = прямое подключение установлено + uint8_t hello_done; // 1 = SUPER_HELLO обмен завершён, можно реплицировать + void* timeout_timer; // таймер ретрансмита + struct CONN_MGR_HANDLE* connection; // собственное CM-владение; восстановлением пути управляет CM + struct media_delivery_ctx* md; // обратная ссылка +}; + +/* per-file download tracking (source node admission control) */ + +struct md_file_load { + struct ll_entry ll; // индекс по media_id (16 байт) + uint8_t media_id[16]; + int active_downloads; // текущее число исходящих стримов + uint64_t downloader_ids[10]; // кто сейчас качает (для редиректа) +}; + +/* + * block_availability: жизненный цикл записей и гарантия уникальности id + * + * Каждое событие создаёт новую запись с новым id (AUTOINCREMENT). + * Старая запись удаляется — id НЕ переиспользуется. + * Это критично для репликации: курсор super_sync.last_recv_id + * всегда монотонно растёт и не пропускает события. + * + * BLOCK_PROCESSING (subcmd 0x10): + * md_ba_insert(... , status=0) — чистый INSERT, новый id=X. + * + * HAVE_BLOCK (subcmd 0x06): + * md_ba_complete_block(): + * 1. DELETE FROM block_availability WHERE block_uuid=? AND node_id=? AND status=0 + * (удаляет processing-запись id=X, если была) + * 2. Если completed уже есть → no-op (возврат 0) + * 3. INSERT ... status=1 — новый id=Y. + * + * SUPER_REPL (subcmd 0x08): + * Принимающая сторона: + * - status=0: INSERT OR IGNORE (дубликаты при репликации — норма) + * - status=1: md_ba_complete_block() — та же логика что HAVE_BLOCK + * + * Итог: processing id=X удаляется, completed вставляется с новым id=Y>X. + * Репликация через md_ba_get_since(id > last_recv_id) всегда видит Y. + */ + +struct media_served_node { + struct ll_entry ll; // индекс по node_id (8 байт) + uint64_t node_id; + uint64_t group_id; + int64_t joined_at; // timestamp регистрации +}; + +#define MD_MAX_RELAY_DOWNSTREAM 5 + +struct relay_downstream { + uint64_t node_id; + uint64_t group_id; + uint64_t sent_offset; // сколько байт уже отправлено этому пиру + struct queue_waiter_handle waiter; +}; + +struct relay_block_ctx { + struct ll_entry ll; // индекс по block_id (16 байт) + uint8_t block_id[16]; + uint8_t media_id[16]; + uint32_t chunk; + uint64_t file_offset; // всего байт получено от апстрима (размер .chunk_N) + char chunk_file[2048]; // путь к .chunk_N файлу для relay-чтения + struct relay_downstream downstream[MD_MAX_RELAY_DOWNSTREAM]; + int downstream_count; + struct UTUN_INSTANCE* inst; +}; + +struct stream_ctx; +struct relay_fwd_ctx; +struct media_delivery_ctx { + struct stream_ctx* streams; + struct relay_fwd_ctx* forwards; + uint64_t self_node_id; + int initialized; + uint8_t is_supernode; // из adm_tags в peers_* + uint8_t active_streams; // текущее число исходящих стримов (admission) + uint8_t max_downloads_per_file; // лимит параллельных загрузок с одного файла (source node), default 3 + uint8_t stream_completed; // 1 = хотя бы один стрим завершился (для тестов) + uint8_t streams_started; // монотонный счётчик запущенных стримов (для тестов) + int dl_stall_timeout_tb; // таймаут простоя загрузки (по умолч. MEDIA_DL_STALL_TIMEOUT_TB) + int dl_max_attempts; // попыток на блок (по умолч. MEDIA_DL_MAX_ATTEMPTS) + sqlite3* db; // = inst->topo_sqlite_db (кешируем для удобства) + struct ll_queue* served_nodes; // media_served_node — только суперузел + struct ll_queue* super_peers; // media_super_peer — только суперузел + struct ll_queue* downloads; // media_download — активные загрузки + struct ll_queue* relay_blocks; // relay_block_ctx — relay в процессе загрузки + struct ll_queue* file_loads; // md_file_load — per-file download count (source node) + struct UTUN_INSTANCE* inst; +}; + +int media_delivery_init(struct UTUN_INSTANCE* inst); +void media_delivery_destroy(struct UTUN_INSTANCE* inst); +int media_delivery_bind(struct UTUN_INSTANCE* inst); +void media_delivery_set_supernode(struct UTUN_INSTANCE* inst, int is_supernode); +void media_delivery_stream_done(struct UTUN_INSTANCE* inst); + +/* Автор анонсирует свои блоки суперузлам CHAT-группы (HAVE_BLOCK), чтобы первый + * скачивающий мог найти держателя через QUERY. Вызывается после регистрации медиа. */ +void media_delivery_announce_media(struct UTUN_INSTANCE* inst, uint64_t group_id, + const uint8_t* media_id, const uint8_t* block_ids, + int num_blocks); + +/* Отправить один HAVE_BLOCK суперузлу (подпись Ed25519 + md_send). Общий helper + * для announce_media / announce_local_blocks / media_download. */ +void media_delivery_send_have_block(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_node_id, + const uint8_t* media_id, const uint8_t* block_id, uint32_t chunk); + +/* Переанонсировать суперузлам ВСЕ локальные блоки (media_files WHERE node_id=self). + * Вызывается при старте после синхронизации, чтобы суперузел восстановил свой + * block_availability для уже скачанных ранее медиа. Идемпотентно на суперузле. */ +void media_delivery_announce_local_blocks(struct UTUN_INSTANCE* inst); + +/* Отменить всё активное, что связано с каналом (локальное удаление канала): + * загрузки с group_id, их relay-контексты и super_peers этой группы. */ +void media_delivery_cancel_channel(struct UTUN_INSTANCE* inst, uint64_t group_id); + +/* relay block context helpers (used by media_download.c) */ +struct relay_block_ctx* md_relay_find(struct media_delivery_ctx* md, const uint8_t* block_id); +struct relay_block_ctx* md_relay_add(struct media_delivery_ctx* md, + const uint8_t* block_id, const uint8_t* media_id, uint32_t chunk); +void md_relay_remove(struct media_delivery_ctx* md, const uint8_t* block_id); + +/* file_load helpers (per-file source download tracking) */ +struct md_file_load* md_file_load_find(struct media_delivery_ctx* md, const uint8_t* media_id); +int md_file_load_inc(struct media_delivery_ctx* md, const uint8_t* media_id, uint64_t node_id); +void md_file_load_dec(struct media_delivery_ctx* md, const uint8_t* media_id, uint64_t node_id); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/media_delivery/media_index.h b/src/media_delivery/media_index.h index b9cebadc..73c1c941 100644 --- a/src/media_delivery/media_index.h +++ b/src/media_delivery/media_index.h @@ -1,5 +1,6 @@ -/* media_index — индекс блоков канального файла в SQLite media_files: UUID, SHA256, Ed25519-подписи и location. - * init создаёт таблицу; register_async копирует/хеширует/подписывает файл в worker, затем commit и callback в uasync. +/* media_index — регистрация файлов канала и их блоков в SQLite для последующей раздачи и сообщений чата. + * Порядок: init → register_async → callback с UUID, хешем и подписями блоков; затем можно анонсировать файл. + * Register_async копирует/хеширует/подписывает файл в worker, затем commit и callback выполняются в uasync. * db/ma/ua заимствованы и должны жить до завершения; доступ к БД выполняется в потоке ядра. * Результат callback заимствован только до возврата. result_free освобождает block_ids/block_sigs, но не саму структуру. */ #ifndef MEDIA_INDEX_H @@ -22,15 +23,23 @@ struct UASYNC; struct media_index_result { uint8_t media_id[16]; uint8_t content_hash[32]; - int64_t file_size; - int64_t block_size; - int num_blocks; + int64_t file_size; // полный размер файла, байты + int64_t block_size; // размер обычного блока, байты; последний может быть короче + int num_blocks; // число блоков и элементов block_ids/block_sigs uint8_t* block_ids; // num_blocks * 16 uint8_t* block_sigs; // num_blocks * 64 }; +// ── Инициализация таблицы ── + +// Создать таблицу media_files и её индексы, сохранив существующие записи; 0/-1. int media_index_init(sqlite3* db); +// ── Регистрация файлов и блоков ── + +/* Записать подготовленные блоки и подписанные сведения о держателе; 0/-1, result остаётся у вызывающего. + * chat_id — десятичный ID канала; dest_path — готовый файл, media_base — база для относительного location. + * Выполняется в uasync, сама функция не копирует/не хеширует файл и не анонсирует блоки. */ int media_index_commit( sqlite3* db, const struct media_index_result* result, uint64_t node_id, const uint8_t* ed25519_privkey, @@ -43,7 +52,10 @@ int media_index_register_downloaded(sqlite3* db, const uint8_t* media_id, const const char* location, uint64_t node_id, int64_t file_size, int64_t chunk_size, int chunk, int64_t offset); -/* Callback может быть синхронным при отказе запуска. err=0 даёт result, иначе result=NULL. */ +/* Подготовить и зарегистрировать файл; пути/ключ/chat_id копируются до возврата, cb_arg заимствован до callback. + * При copy_file!=0 копирует src_path в dest_path; при 0 dest_path уже должен содержать готовый файл. + * Файлы и db/ma/ua должны жить до callback. Callback может быть синхронным при отказе запуска. + * err=0 даёт result только на время callback; иначе result=NULL. Сохранить нужные поля/массивы отдельной копией. */ void media_index_register_async( struct media_async* ma, struct UASYNC* ua, sqlite3* db, uint64_t node_id, const uint8_t* ed25519_privkey, @@ -53,10 +65,19 @@ void media_index_register_async( void (*cb)(void* arg, int err, const struct media_index_result* result), void* cb_arg); +// ── Вспомогательные операции ── + +// Заполнить 16 байт идентификатора; возвращает 0. int media_index_generate_uuid(uint8_t uuid_out[16]); + +// Освободить только массивы собственного result и обнулить их указатели; callback-result освобождает сам модуль. void media_index_result_free(struct media_index_result* result); + +// Выбрать размер блока в байтах: 10..25 МиБ для непустого файла, 0 для file_size=0. int media_calc_block_size(uint64_t file_size); +// ── Локальное удаление канала ── + /* Удалить медиа канала (локальное удаление канала): стереть файлы на диске * (media_base + location) и строки media_files с chat_id. Возвращает 0/ошибка. */ int media_index_delete_channel(sqlite3* db, const char* chat_id, const char* media_base); diff --git a/src/media_delivery/media_index.h1 b/src/media_delivery/media_index.h1 new file mode 100644 index 00000000..b9cebadc --- /dev/null +++ b/src/media_delivery/media_index.h1 @@ -0,0 +1,67 @@ +/* media_index — индекс блоков канального файла в SQLite media_files: UUID, SHA256, Ed25519-подписи и location. + * init создаёт таблицу; register_async копирует/хеширует/подписывает файл в worker, затем commit и callback в uasync. + * db/ma/ua заимствованы и должны жить до завершения; доступ к БД выполняется в потоке ядра. + * Результат callback заимствован только до возврата. result_free освобождает block_ids/block_sigs, но не саму структуру. */ +#ifndef MEDIA_INDEX_H +#define MEDIA_INDEX_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +struct media_async; +struct UASYNC; + +#define MEDIA_ID_SIZE 16 +#define MEDIA_HASH_SIZE 32 + +struct media_index_result { + uint8_t media_id[16]; + uint8_t content_hash[32]; + int64_t file_size; + int64_t block_size; + int num_blocks; + uint8_t* block_ids; // num_blocks * 16 + uint8_t* block_sigs; // num_blocks * 64 +}; + +int media_index_init(sqlite3* db); + +int media_index_commit( + sqlite3* db, const struct media_index_result* result, + uint64_t node_id, const uint8_t* ed25519_privkey, + const char* chat_id, const char* dest_path, const char* media_base); + +/* Зарегистрировать скачанный блок в media_files (location — путь к собранному файлу), + * чтобы узел мог отдавать его как держатель. node_sign = нули (при отдаче не проверяется). */ +int media_index_register_downloaded(sqlite3* db, const uint8_t* media_id, const uint8_t* block_id, + const uint8_t* content_hash, const char* chat_id, + const char* location, uint64_t node_id, + int64_t file_size, int64_t chunk_size, int chunk, int64_t offset); + +/* Callback может быть синхронным при отказе запуска. err=0 даёт result, иначе result=NULL. */ +void media_index_register_async( + struct media_async* ma, struct UASYNC* ua, sqlite3* db, + uint64_t node_id, const uint8_t* ed25519_privkey, + const char* chat_id, + const char* src_path, const char* dest_path, int copy_file, + const char* media_base, + void (*cb)(void* arg, int err, const struct media_index_result* result), + void* cb_arg); + +int media_index_generate_uuid(uint8_t uuid_out[16]); +void media_index_result_free(struct media_index_result* result); +int media_calc_block_size(uint64_t file_size); + +/* Удалить медиа канала (локальное удаление канала): стереть файлы на диске + * (media_base + location) и строки media_files с chat_id. Возвращает 0/ошибка. */ +int media_index_delete_channel(sqlite3* db, const char* chat_id, const char* media_base); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/member_files/member_files.h b/src/member_files/member_files.h index c1ce2f6b..2b111bef 100644 --- a/src/member_files/member_files.h +++ b/src/member_files/member_files.h @@ -1,7 +1,8 @@ -/* Хранилище участников и асинхронный клиент поверх подписанного/зашифрованного router. - * Все API — uasync. callback получает заимствованный response ровно один раз; операция - * уже освобождена. NULL при отказе запуска, callback не вызывается. request копируется. - * quiesce запрещает новые команды и отменяет producers; destroy вне callbacks ждёт +/* member_files — файловые каталоги участников на storage-узлах CHAT-группы и клиент доступа к ним. + * Для списка/операций: request с командой из protocol.h → callback с ответом. Для файла: transfer_start → progress/finished. + * Клиент указывает группу, storage-сервер, владельца каталога и путь; права проверяются по членству и роли узла. + * Все API — uasync. Запрос копируется; callback получает заимствованный ответ, действительный только до возврата. + * Чат-сервис вызывает init; quiesce запрещает новые команды и отменяет producers; destroy вне callbacks ждёт * собственный дисковый поток до удаления CHAT-групп. Cancel не удаляет опубликованные файлы. * GUI-команды используют trampolines; результат — CHAT_EVT_MEMBER_FILES_RESPONSE/TRANSFER. */ #ifndef MEMBER_FILES_H @@ -13,43 +14,82 @@ extern "C" { struct UTUN_INSTANCE; struct mf_pending; typedef void (*mf_response_fn)(void* arg, const struct mf_message* response); + +// ── Жизненный цикл модуля ── + +// Создать состояние сервера/клиента и запустить дисковый worker; 0/-1. int member_files_init(struct UTUN_INSTANCE* inst); + +// Запретить новые команды и начать отмену запросов/передач; запущенный дисковый I/O завершается отдельно. void member_files_quiesce(struct UTUN_INSTANCE* inst); + +// Дождаться дискового worker и освободить контекст; вне callbacks, до удаления CHAT-групп. void member_files_destroy(struct UTUN_INSTANCE* inst); + +// Отменить запросы и передачи одной группы перед её удалением. void member_files_cancel_group(struct UTUN_INSTANCE* inst, uint64_t group); + +// ── Команды файлового сервера ── + +/* Отправить одну команду; request_id=0 выбирает случайный ID. NULL — отказ запуска, callback не вызывается. + * При успехе callback ровно один, включая отмену; handle освобождён до callback, arg принадлежит вызывающему. */ struct mf_pending* member_files_request(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t server, const struct mf_message* request, mf_response_fn callback, void* arg); + +// Синхронно завершить свой запрос ответом MF_CANCELLED; handle после вызова недействителен, NULL допустим. void member_files_cancel(struct mf_pending* pending); + +// Отменить pending-запрос с этим request_id; не отменяет потоковую передачу файла. void member_files_cancel_request_id(struct UTUN_INSTANCE* inst, uint64_t id); -/* 1 — действующий мембер; storage!=0 дополнительно требует подписанную роль хранителя. */ + +// ── Проверка доступа ── + +// 1 — действующий мембер; storage!=0 дополнительно требует подписанную роль хранителя. int member_files_member(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t node, int storage); +// ── Команда интерфейса ── + struct mf_gui_request { struct UTUN_INSTANCE* inst; uint64_t group, server; struct mf_message message; }; + +// arg — u_malloc/u_calloc mf_gui_request; освобождается после ответа CHAT_EVT_MEMBER_FILES_RESPONSE. void member_files_gui_request(void* arg); +// ── Потоковая передача файлов ── + struct mf_transfer_request { - struct UTUN_INSTANCE* inst; - uint64_t id, group, server, owner; + struct UTUN_INSTANCE* inst; // ядро с работающим чат-сервисом + uint64_t id, group, server, owner; // ID операции (0=авто), CHAT-группа, storage-узел и UID владельца каталога uint64_t max_bytes; // 0 — без лимита; проверяется до открытия локального файла и передачи данных. - int upload; - uint8_t version[16]; - char remote[MF_PATH_MAX + 1], local[MF_PATH_MAX + 1]; + int upload; // ненулевой — загрузить local на сервер, 0 — скачать remote + uint8_t version[16]; // версия из STAT; нули при создании нового серверного файла + char remote[MF_PATH_MAX + 1], local[MF_PATH_MAX + 1]; // путь в каталоге owner и локальный путь приложения }; struct mf_transfer; struct mf_transfer_progress { uint64_t id, done, total; uint16_t status; uint8_t finished; }; typedef void (*mf_transfer_fn)(void* arg, const struct mf_transfer_progress* progress); + // Один handle на файл; до 16 клиентских передач одновременно, включая upload/download к одному пиру. // Все вызовы/callbacks — uasync. id=0 выбирает случайный ID. NULL: отказ запуска, callback не вызывается. // finished приходит ровно один раз после закрытия/публикации; handle уже освобождён до callback. // Прогресс заимствован на время callback. Cancel асинхронно ждёт свой I/O и не затрагивает другие handles. +// Request копируется; исходную структуру можно освободить сразу, arg должен жить до finished. struct mf_transfer* member_files_transfer_start(const struct mf_transfer_request* request, mf_transfer_fn callback, void* arg); + +// Запросить отмену своей передачи; завершение приходит через finished после закрытия её файлового I/O. void member_files_transfer_cancel(struct mf_transfer* transfer); + +// ── Команды интерфейса для передачи и отмены ── + /* id выбирает вызывающий. События: id/group/server/owner/done/total:8LE, status:2LE,state:1; - * state=1 progress, 2 finished. Загрузка/скачивание выполняет общий клиент, без Qt I/O. */ + * state=1 progress, 2 finished. arg — u_malloc/u_calloc mf_transfer_request, освобождается после finished. */ void member_files_gui_transfer(void* arg); + struct mf_cancel_request { struct UTUN_INSTANCE* inst; uint64_t id; }; + +// arg — u_malloc/u_calloc mf_cancel_request, освобождается здесь; отменяет передачу либо одиночный запрос по id. void member_files_gui_cancel(void* arg); + #ifdef __cplusplus } #endif diff --git a/src/member_files/member_files.h1 b/src/member_files/member_files.h1 new file mode 100644 index 00000000..c1ce2f6b --- /dev/null +++ b/src/member_files/member_files.h1 @@ -0,0 +1,56 @@ +/* Хранилище участников и асинхронный клиент поверх подписанного/зашифрованного router. + * Все API — uasync. callback получает заимствованный response ровно один раз; операция + * уже освобождена. NULL при отказе запуска, callback не вызывается. request копируется. + * quiesce запрещает новые команды и отменяет producers; destroy вне callbacks ждёт + * собственный дисковый поток до удаления CHAT-групп. Cancel не удаляет опубликованные файлы. + * GUI-команды используют trampolines; результат — CHAT_EVT_MEMBER_FILES_RESPONSE/TRANSFER. */ +#ifndef MEMBER_FILES_H +#define MEMBER_FILES_H +#include "protocol.h" +#ifdef __cplusplus +extern "C" { +#endif +struct UTUN_INSTANCE; +struct mf_pending; +typedef void (*mf_response_fn)(void* arg, const struct mf_message* response); +int member_files_init(struct UTUN_INSTANCE* inst); +void member_files_quiesce(struct UTUN_INSTANCE* inst); +void member_files_destroy(struct UTUN_INSTANCE* inst); +void member_files_cancel_group(struct UTUN_INSTANCE* inst, uint64_t group); +struct mf_pending* member_files_request(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t server, + const struct mf_message* request, mf_response_fn callback, void* arg); +void member_files_cancel(struct mf_pending* pending); +void member_files_cancel_request_id(struct UTUN_INSTANCE* inst, uint64_t id); +/* 1 — действующий мембер; storage!=0 дополнительно требует подписанную роль хранителя. */ +int member_files_member(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t node, int storage); + +struct mf_gui_request { struct UTUN_INSTANCE* inst; uint64_t group, server; struct mf_message message; }; +void member_files_gui_request(void* arg); + +struct mf_transfer_request { + struct UTUN_INSTANCE* inst; + uint64_t id, group, server, owner; + uint64_t max_bytes; // 0 — без лимита; проверяется до открытия локального файла и передачи данных. + int upload; + uint8_t version[16]; + char remote[MF_PATH_MAX + 1], local[MF_PATH_MAX + 1]; +}; + +struct mf_transfer; +struct mf_transfer_progress { uint64_t id, done, total; uint16_t status; uint8_t finished; }; +typedef void (*mf_transfer_fn)(void* arg, const struct mf_transfer_progress* progress); +// Один handle на файл; до 16 клиентских передач одновременно, включая upload/download к одному пиру. +// Все вызовы/callbacks — uasync. id=0 выбирает случайный ID. NULL: отказ запуска, callback не вызывается. +// finished приходит ровно один раз после закрытия/публикации; handle уже освобождён до callback. +// Прогресс заимствован на время callback. Cancel асинхронно ждёт свой I/O и не затрагивает другие handles. +struct mf_transfer* member_files_transfer_start(const struct mf_transfer_request* request, mf_transfer_fn callback, void* arg); +void member_files_transfer_cancel(struct mf_transfer* transfer); +/* id выбирает вызывающий. События: id/group/server/owner/done/total:8LE, status:2LE,state:1; + * state=1 progress, 2 finished. Загрузка/скачивание выполняет общий клиент, без Qt I/O. */ +void member_files_gui_transfer(void* arg); +struct mf_cancel_request { struct UTUN_INSTANCE* inst; uint64_t id; }; +void member_files_gui_cancel(void* arg); +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/routing_layer/multicast.h b/src/routing_layer/multicast.h index a0900568..484358a8 100644 --- a/src/routing_layer/multicast.h +++ b/src/routing_layer/multicast.h @@ -1,8 +1,13 @@ /* multicast — отправка одного payload явно указанному списку узлов topo-группы. * Общая часть пути передаётся одним пакетом; при расхождении маршрутов пакет * разделяется на ветки с соответствующими подмножествами получателей. - * - * Как отправлять: + * Источник: open → сообщить свой дескриптор получателям → send → close. + * Получатель: subscribe на источник/дескриптор до отправки DATA → on_data → unsubscribe. + * Все операции — uasync; callbacks отложены, их данные заимствованы до возврата. + * Доставка без сквозных ACK/повторов; потери приходят через on_loss, но сам отчёт тоже может потеряться. */ + +/* Подробные правила дескрипторов, подписок и владения. + * Отправка: * 1. multicast_open(group, on_loss, arg) выделяет исходящий дескриптор. * Номер 1..65535 уникален среди открытых дескрипторов данного ядра. * Выдача последовательная: 1, 2, ... 65535, затем снова с 1, пропуская @@ -85,7 +90,7 @@ struct multicast_subscription; /* непрозрачный handle локаль #define MULTICAST_REPORT_BYTES (16u * 1024u) /* общий бюджет накопленных отчётов и окон троттлинга */ #define MULTICAST_REPORT_INTERVAL_TB 3000 /* 300 мс; timebase = 0.1 мс */ -/* Размеры служебной оболочки; payload вместе с ней должен помещаться в 16 КиБ ETCP. */ +// Размеры служебной оболочки; payload вместе с ней должен помещаться в 16 КиБ ETCP. #define MULTICAST_MAX_VISITED 64 #define MULTICAST_HEADER_SIZE 21 #define MULTICAST_PATH_RESERVE (8 * MULTICAST_MAX_VISITED + 1) @@ -143,6 +148,8 @@ struct multicast_stats { unsigned subscriptions; /* активные входящие подписки */ }; +// ── API отправителя ── + /* Открыть исходящий дескриптор в работающей group. on_loss обязателен; arg может быть NULL. * Выдать следующий свободный номер по циклическому порядку 1..65535, начиная после * последнего выданного; 0 не выдаётся. Закрытые номера используются только при обходе. @@ -167,6 +174,8 @@ int multicast_send(struct UTUN_INSTANCE* inst, uint16_t descriptor, const uint64 * Для отсутствующего дескриптора — диагностируемая ошибка без побочных действий. */ void multicast_close(struct UTUN_INSTANCE* inst, uint16_t descriptor); +// ── API получателя ── + /* Подписаться на локальную доставку (group_id, src_node, src_descriptor). * src_node!=0, src_descriptor!=0, on_data обязателен; arg может быть NULL. * Возврат: принадлежащий вызывающему handle либо NULL с диагностикой ошибки. @@ -181,18 +190,25 @@ struct multicast_subscription* multicast_subscribe(struct TOPO_GROUP* group, uin * Из собственного on_data допускается; память освобождается после его завершения. */ void multicast_unsubscribe(struct multicast_subscription* subscription); +// ── Диагностика ── + /* Снимок общих счётчиков ядра в *out. 0=успех, <0=неверный аргумент/модуль не создан. * Не открывает дескрипторов или подписок, счётчики не сбрасывает. */ int multicast_get_stats(struct UTUN_INSTANCE* inst, struct multicast_stats* out); +// ── Внутренняя интеграция с ядром ── + /* Далее — интеграция с ядром; сервисы используют open/send/close и subscribe/unsubscribe. * init вызывается ядром один раз после etcp_router_init; 0=успех, <0=ошибка. * destroy отменяет работу, закрывает дескрипторы, деактивирует подписки; повтор допустим. * Вызывать до уничтожения router/ETCP и вне callbacks модуля. */ int multicast_init(struct UTUN_INSTANCE* inst); + void multicast_destroy(struct UTUN_INSTANCE* inst); -/* Запланировать пересмотр ожидающих DATA после изменения маршрутов; повторные вызовы объединяются. */ + +// Запланировать пересмотр ожидающих DATA после изменения маршрутов; повторные вызовы объединяются. void multicast_routes_changed(struct UTUN_INSTANCE* inst); + /* Перед удалением группы: закрыть её дескрипторы, деактивировать подписки, * убрать её DATA/отчёты из общих очередей, сохранив работу остальных групп. */ void multicast_close_group(struct UTUN_INSTANCE* inst, uint64_t group_id); diff --git a/src/routing_layer/multicast.h1 b/src/routing_layer/multicast.h1 new file mode 100644 index 00000000..a0900568 --- /dev/null +++ b/src/routing_layer/multicast.h1 @@ -0,0 +1,219 @@ +/* multicast — отправка одного payload явно указанному списку узлов topo-группы. + * Общая часть пути передаётся одним пакетом; при расхождении маршрутов пакет + * разделяется на ветки с соответствующими подмножествами получателей. + * + * Как отправлять: + * 1. multicast_open(group, on_loss, arg) выделяет исходящий дескриптор. + * Номер 1..65535 уникален среди открытых дескрипторов данного ядра. + * Выдача последовательная: 1, 2, ... 65535, затем снова с 1, пропуская + * занятые номера. Счётчик общий для всех групп ядра; close его не откатывает. + * Группа и обработчик потерь закрепляются за ним до закрытия. + * 2. Передать получателям свой node_id и номер дескриптора через протокол + * использующего сервиса. Получатели должны установить подписки до DATA. + * 3. multicast_send(inst, descriptor, recipients, count, payload, len). + * Можно отправлять много пакетов; список адресатов задаётся для каждого. + * Список и payload копируются: после возврата их можно менять/освобождать. + * 4. Перегрузки и локальные ошибки приходят в on_loss этого дескриптора. + * loss->descriptor указывает исходящий дескриптор, loss->recipients — + * только пострадавшую часть списка. Решение о снижении нагрузки принимает сервис. + * 5. multicast_close(inst, descriptor) завершает использование дескриптора. + * + * Как принимать: + * 1. Получить node_id источника и ЕГО исходящий дескриптор от сервиса. + * 2. multicast_subscribe(group, src_node, src_descriptor, on_data, arg). + * Подписка локальная, ключ — (group_id, src_node, src_descriptor). + * Она не посылает JOIN и не добавляет нас в список адресатов у источника. + * Собственный исходящий дескриптор для приёма выделять не требуется. + * 3. on_data вызывается только для пакетов, где мы указаны получателем. + * Без подписки локальная доставка тихо отбрасывается, без лога и отчёта. + * Транзит другим получателям продолжается независимо от наших подписок. + * 4. multicast_unsubscribe(subscription) прекращает приём и освобождает handle. + * + * Минимальный порядок (на разных узлах; on_loss/on_data — callbacks сервиса): + * int fd = multicast_open(send_group, on_loss, sender_ctx); + * if (fd < 0) ...; // дескриптор не создан + * // сервис передаёт source_node_id и (uint16_t)fd получателю + * struct multicast_subscription* sub = + * multicast_subscribe(recv_group, source_node_id, (uint16_t)fd, on_data, receiver_ctx); + * if (!sub) ...; // подписка не создана + * // после готовности подписки, на источнике: + * int rc = multicast_send(send_inst, (uint16_t)fd, receivers, count, data, size); + * if (rc < 0) ...; // этот вызов не принят + * // при завершении, каждый на своём узле: + * multicast_unsubscribe(sub); + * multicast_close(send_inst, (uint16_t)fd); + * + * Условия использования: + * - Все вызовы и callbacks выполняются в uasync-потоке соответствующего ядра. + * - Callbacks отложены: open/subscribe/send/close/unsubscribe их не вызывают. + * В callback можно отправлять, закрывать свой дескриптор и отписывать себя. + * Уничтожать ядро/группу/модуль из callback нельзя. + * - payload, loss и loss->recipients заимствованы до возврата из callback. + * arg принадлежит сервису: он должен жить до close/unsubscribe и завершения + * уже выполняющегося callback. После закрытия новых callbacks не будет. + * - Подпиской владеет вызывающий: unsubscribe обязателен даже после удаления + * группы. Удаление группы/ядра деактивирует подписку, но handle остаётся + * валиден до unsubscribe; после unsubscribe использовать его нельзя. + * - Удаление группы автоматически закрывает её исходящие дескрипторы. + * - Успех send означает принятие к обработке, а не доставку. Возможны частичные + * потери; сквозных ACK/повторов нет, порядок при смене маршрута не гарантируется. + * Сам отчёт тоже может потеряться: отсутствие on_loss не подтверждает доставку. + * + * Локальные callbacks DATA тоже отложены; их общая очередь ограничена 256 КиБ + * памяти. Отказ её выделения/переполнение считается MULTICAST_INTERNAL_ERROR. + * Поколений нет: DATA, отчёты и подписки используют 16-битный номер дескриптора. + * Новое ядро начинает выдачу с 1. После повторной выдачи номера запоздалый + * сетевой пакет с тем же источником и номером неотличим от пакета нового владельца. + */ +#ifndef UTUN_MULTICAST_H +#define UTUN_MULTICAST_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct UTUN_INSTANCE; +struct TOPO_GROUP; +struct multicast_ctx; +struct multicast_subscription; /* непрозрачный handle локальной подписки; освобождает вызывающий */ + +#define MULTICAST_DESCRIPTOR_INVALID 0 +#define MULTICAST_QUEUE_BYTES (256u * 1024u) /* одна FIFO на ETCP-соединение для всех рассылок */ +#define MULTICAST_REPORT_BYTES (16u * 1024u) /* общий бюджет накопленных отчётов и окон троттлинга */ +#define MULTICAST_REPORT_INTERVAL_TB 3000 /* 300 мс; timebase = 0.1 мс */ + +/* Размеры служебной оболочки; payload вместе с ней должен помещаться в 16 КиБ ETCP. */ +#define MULTICAST_MAX_VISITED 64 +#define MULTICAST_HEADER_SIZE 21 +#define MULTICAST_PATH_RESERVE (8 * MULTICAST_MAX_VISITED + 1) +#define MULTICAST_REPORT_HEADER_SIZE 41 + +enum multicast_loss_reason { + MULTICAST_OVERLOAD = 1, /* очередь выбранного исходящего соединения заполнена */ + MULTICAST_NO_ROUTE, /* только локальное уведомление источнику */ + MULTICAST_PATH_LOOP, /* локальное уведомление; транзит логирует потерю */ + MULTICAST_INTERNAL_ERROR /* выделение памяти / отказ транспорта */ +}; + +struct multicast_loss { + uint64_t group_id; /* группа исходной рассылки */ + uint64_t source_node_id; /* источник исходного DATA */ + uint64_t src, dst; /* узел потери и выбранный next hop; dst=0 без маршрута */ + uint16_t descriptor; /* исходящий дескриптор, callback которого получает этот отчёт */ + uint16_t recipient_count; /* число ID в recipients */ + const uint64_t* recipients; /* заимствованный массив в host byte order */ + uint32_t packets; /* потери для каждого получателя этого списка */ + enum multicast_loss_reason reason; /* перегрузка либо локальная причина потери */ + uint8_t prefix_len; /* 0..16 значимых байт */ + uint8_t prefix[16]; /* начало payload первого потерянного пакета */ +}; + +/* Приём для конкретной подписки. len может быть 0; payload нельзя сохранять без копирования. + * subscription можно передать в unsubscribe прямо здесь; после этого handle недействителен. + * src_node/src_descriptor/group известны сервису из subscribe и могут храниться в arg. */ +typedef void (*multicast_recv_fn)(struct multicast_subscription* subscription, const uint8_t* payload, size_t len, void* arg); + +/* Уведомление конкретному исходящему дескриптору. loss->src/dst — перегруженный + * участок, не пара «исходный отправитель / конечный получатель». + * packets относится к каждому ID в recipients. При разделении отчёта на части + * нельзя складывать packets как число уникальных исходных пакетов. + * Допустим multicast_close(inst, loss->descriptor) из этого callback. */ +typedef void (*multicast_loss_fn)(struct UTUN_INSTANCE* inst, const struct multicast_loss* loss, void* arg); + +struct multicast_stats { + uint64_t sent; /* ветки DATA, переданные ETCP */ + uint64_t delivered; /* вызовы обработчиков входящих подписок */ + uint64_t unsubscribed; /* тихие отбрасывания локальной доставки без подписки */ + uint64_t overload_packets; /* ветки, отброшенные из-за лимита исходящей FIFO */ + uint64_t no_route_packets; /* ветки без доступного маршрута */ + uint64_t loop_packets; /* петли и превышения длины пройденного пути */ + uint64_t invalid_packets; /* некорректные DATA/отчёты */ + uint64_t internal_errors; /* отказы выделения памяти или передачи транспорту */ + uint64_t reports_sent; /* переданные части отчётов, включая локальные уведомления */ + uint64_t reports_received; /* принятые части отчётов */ + uint64_t reports_evicted; /* записи отчётов, вытесненные из бюджета 16 КиБ */ + size_t queued_bytes; /* DATA в FIFO модуля, без уже переданного ETCP */ + size_t report_bytes; /* память накопленных отчётов и окон, без переданного router */ + size_t delivery_bytes; /* память DATA, ожидающих локальные callbacks; максимум 256 КиБ */ + unsigned queues; /* лениво созданные FIFO, в том числе временно опустевшие */ + unsigned descriptors; /* открытые исходящие дескрипторы */ + unsigned subscriptions; /* активные входящие подписки */ +}; + +/* Открыть исходящий дескриптор в работающей group. on_loss обязателен; arg может быть NULL. + * Выдать следующий свободный номер по циклическому порядку 1..65535, начиная после + * последнего выданного; 0 не выдаётся. Закрытые номера используются только при обходе. + * Возврат: 1..65535 — выделенный номер, <0 — ошибка (включая отсутствие свободных номеров). + * До проверки результата не приводить его к uint16_t. Подписку на приём не создаёт. */ +int multicast_open(struct TOPO_GROUP* group, multicast_loss_fn on_loss, void* arg); + +/* Отправить через открытый descriptor, принадлежащий inst; группа берётся из дескриптора. + * recipients: count>0 ненулевых node_id; повторы удаляются, собственный ID допустим. + * len может быть 0; при len>0 payload обязателен. Размер payload, списка и полного + * резерва пройденного пути должен помещаться в 16 КиБ. + * 0=принято к обработке; <0=вызов не принят, ничего не отправлено. + * После принятия отдельные ветки могут потеряться; уведомления идут в on_loss. + * Обе входные области копируются и после возврата больше не используются. */ +int multicast_send(struct UTUN_INSTANCE* inst, uint16_t descriptor, const uint64_t* recipients, + uint16_t count, const void* payload, size_t len); + +/* Закрыть исходящий descriptor данного inst: убрать его из поиска, отменить + * ещё не переданные DATA и ожидающие локальные callbacks этого дескриптора. + * Уже переданное транспорту/другим узлам отозвать нельзя. Удалённые подписки + * автоматически не закрываются. Сохранённый номер после close не использовать. + * Для отсутствующего дескриптора — диагностируемая ошибка без побочных действий. */ +void multicast_close(struct UTUN_INSTANCE* inst, uint16_t descriptor); + +/* Подписаться на локальную доставку (group_id, src_node, src_descriptor). + * src_node!=0, src_descriptor!=0, on_data обязателен; arg может быть NULL. + * Возврат: принадлежащий вызывающему handle либо NULL с диагностикой ошибки. + * Повторная подписка на тот же ключ запрещена; сначала unsubscribe прежней. + * Никаких сетевых запросов не отправляет и пропущенные ранее DATA не воспроизводит. */ +struct multicast_subscription* multicast_subscribe(struct TOPO_GROUP* group, uint64_t src_node, uint16_t src_descriptor, + multicast_recv_fn on_data, void* arg); + +/* Прекратить доставку, отменить ожидающие callbacks и освободить handle. + * NULL допустим. Работает и для подписки, деактивированной удалением группы/ядра. + * После вызова handle недействителен; повторный вызов с тем же указателем запрещён. + * Из собственного on_data допускается; память освобождается после его завершения. */ +void multicast_unsubscribe(struct multicast_subscription* subscription); + +/* Снимок общих счётчиков ядра в *out. 0=успех, <0=неверный аргумент/модуль не создан. + * Не открывает дескрипторов или подписок, счётчики не сбрасывает. */ +int multicast_get_stats(struct UTUN_INSTANCE* inst, struct multicast_stats* out); + +/* Далее — интеграция с ядром; сервисы используют open/send/close и subscribe/unsubscribe. + * init вызывается ядром один раз после etcp_router_init; 0=успех, <0=ошибка. + * destroy отменяет работу, закрывает дескрипторы, деактивирует подписки; повтор допустим. + * Вызывать до уничтожения router/ETCP и вне callbacks модуля. */ +int multicast_init(struct UTUN_INSTANCE* inst); +void multicast_destroy(struct UTUN_INSTANCE* inst); +/* Запланировать пересмотр ожидающих DATA после изменения маршрутов; повторные вызовы объединяются. */ +void multicast_routes_changed(struct UTUN_INSTANCE* inst); +/* Перед удалением группы: закрыть её дескрипторы, деактивировать подписки, + * убрать её DATA/отчёты из общих очередей, сохранив работу остальных групп. */ +void multicast_close_group(struct UTUN_INSTANCE* inst, uint64_t group_id); + +/* Wire DATA, все числа little-endian: + * cmd:1 | group:8 | descriptor:2 | count:2 | source:8 | recipients:8*count | + * payload | visited:8*hops | hops:1. + * Получатели строго возрастают. Первый visited=source, последний=ETCP peer. + * Транзит добавляет себя один раз; повторное посещение запрещено. + * + * Wire OVERLOAD — payload отдельного router-сервиса: + * descriptor:2 | src:8 | dst:8 | packets:4 | count:2 | prefix_len:1 | + * prefix:16 | recipients:8*count. + * src/dst — концы перегруженного участка; router dst — исходный источник DATA. + * Разные наборы потерянных получателей не смешиваются. Префикс относится + * к первому потерянному пакету записи; для payload короче 16 байт остаток нулевой. + * Первая потеря сообщается сразу, последующие накапливаются до конца окна 300 мс. + * Таймер отправляет остаток независимо от поступления новых DATA. + */ + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/stream/stream.h b/src/stream/stream.h index 51227c9e..51678a7f 100644 --- a/src/stream/stream.h +++ b/src/stream/stream.h @@ -1,5 +1,11 @@ -/* Стриминг CHAT-сервиса: локальные handles владеют общим экземпляром трансляции. - * Все API — uasync-поток. Последний reader close освобождает экземпляр; publisher close после FIN +/* stream — публикация и приём трансляций CHAT-группы с подписью источника и восстановлением потерь через Reed–Solomon. + * Источник: publish_open → publish_write или publish_block → publish_finish → close. + * Приём: subscribe_open для событий байтового потока либо media_open → media_read для аудио/видео; затем close. + * Данные хранятся в общем кольце; у каждого локального читателя свой handle. Все вызовы — uasync-поток. + * Коды результатов — STREAM_* из stream_proto.h; STREAM_OK означает успех, STREAM_WAIT — данные ещё не готовы. */ + +/* Владение и callbacks: + * Последний reader close освобождает экземпляр; publisher close после FIN * сохраняет кольцо для локальных readers, без FIN отсоединяет их. Отсоединённый handle также закрывает вызывающий. * DATA/GAP/END/CLOSED/AVAILABLE callback отложен, data/описание заимствованы до его возврата. * Close handles из callback допустим; остановка сервиса/группы и destroy ядра — вне callbacks. @@ -18,7 +24,7 @@ struct UTUN_INSTANCE; struct STREAM_HANDLE; struct stream_service; -struct stream_parameters { uint8_t n, r; uint16_t block_size; }; /* block_size — максимум RS-части. */ +struct stream_parameters { uint8_t n, r; uint16_t block_size; }; // n — всего RS-частей, r — проверочных; block_size — максимум части. enum stream_event_type { STREAM_EVENT_DATA, STREAM_EVENT_GAP, STREAM_EVENT_END, STREAM_EVENT_CLOSED, STREAM_EVENT_AVAILABLE }; enum stream_close_reason { @@ -27,21 +33,21 @@ enum stream_close_reason { }; struct stream_event { - struct stream_descriptor descriptor; - enum stream_event_type type; - uint64_t first_seq, last_seq; - const uint8_t *data; - size_t length; - int reason; + struct stream_descriptor descriptor; // источник, группа и эпоха трансляции + enum stream_event_type type; // DATA, GAP, END, CLOSED или готовность медиа AVAILABLE + uint64_t first_seq, last_seq; // DATA/GAP: включительный диапазон пачек; END: последняя пачка, UINT64_MAX при пустом потоке + const uint8_t *data; // DATA: восстановленные байты, заимствованы до возврата callback + size_t length; // размер data в байтах + int reason; // CLOSED: stream_close_reason }; typedef void (*stream_event_fn)(struct UTUN_INSTANCE *inst, const struct stream_event *event, void *arg); struct stream_info { - struct stream_descriptor descriptor; - struct stream_buffer_stats buffer; - unsigned owners; - int publisher; - int finished; + struct stream_descriptor descriptor; // источник, эпоха и параметры RS + struct stream_buffer_stats buffer; // текущее состояние общего кольца + unsigned owners; // число локальных handles общей сессии + int publisher; // 1 — сессия содержит локальный источник + int finished; // 1 — источник завершён; оставшиеся данные ещё могут читаться }; struct stream_candidate { @@ -57,64 +63,121 @@ struct stream_selection_info { int automatic, protected_by_fec; }; +// ── Жизненный цикл сервиса ── + +// Чат-сервис создаёт общий контекст до открытия handles; STREAM_OK или отрицательная ошибка. int stream_service_init(struct UTUN_INSTANCE *inst); + +// Остановить трансляции и отсоединить handles; вызывающий закрывает оставшиеся handles сам. Вне callbacks. void stream_service_destroy(struct UTUN_INSTANCE *inst); + +// Отсоединить трансляции группы при её удалении; handles остаются у владельцев до close. void stream_cancel_group(struct UTUN_INSTANCE *inst, uint64_t group_id); + +// ── События и каталог источников ── + +// Единственный общий обработчик событий экземпляра, arg живёт до снятия; callback=NULL отключает доставку. void stream_set_event_callback(struct UTUN_INSTANCE *inst, stream_event_fn callback, void *arg); + /* Проверенные online-публикации: JSON с id(decimal string), epoch(hex string), name, self. * Только uasync; *out заранее NULL, результат освобождается u_free(). WAIT — группа недоступна. */ int stream_sources_json(struct UTUN_INSTANCE *inst, uint64_t group_id, char **out); + /* Независимое наблюдение owner: начальный снимок и изменения через CHAT_EVT_STREAM_SOURCES. * group_id=0 снимает только этого owner; owner=0 разрешён для выбранного канала frontend. * Недоступный каталог имеет valid=0 и не означает завершение источников. * Не создаёт подписку/буфер и не удерживает ACTIVE. Только uasync. */ int stream_watch_sources(struct UTUN_INSTANCE *inst, uint64_t group_id, uint64_t owner); -/* Локальный owner удерживает эффективный ACTIVE; желаемый frontend-режим ядро восстанавливает после последнего close. */ + +// ── Согласование с активностью ядра и рацией ── + +// Локальный owner удерживает эффективный ACTIVE; желаемый frontend-режим ядро восстанавливает после последнего close. int stream_service_needs_activity(const struct UTUN_INSTANCE *inst); -/* Только локальный viewer этой группы/эпохи подавляет рацию источника со включённым звуком. */ + +// Только локальный viewer этой группы/эпохи подавляет рацию источника со включённым звуком. int stream_radio_muted(struct UTUN_INSTANCE *inst, uint64_t group_id, uint64_t source_id); -/* out заранее NULL. parameters=NULL — 6+2, часть до 1300 байт и одна подпись пачки внутри RS. - * Одна публикация этого узла в группе; новый запуск получает новый source_epoch. */ +// ── Открытие источника и подписок ── + +/* Открыть источник; *out заранее NULL. parameters=NULL использует STREAM_DEFAULT_N/R: 8 частей без избыточности. + * Максимальный размер части задаёт STREAM_RS_DEFAULT_SIZE; одна подпись защищает всю пачку внутри RS. + * Одна публикация этого узла в группе; новый запуск получает новый source_epoch. При успехе handle закрывает вызывающий. */ int stream_publish_open(struct UTUN_INSTANCE *inst, uint64_t group_id, const struct stream_parameters *parameters, struct STREAM_HANDLE **out); -/* Меняет подписанное состояние звука, не формат медиатреков. Только uasync; raw publisher начинает с 0. */ + +// Меняет подписанное состояние звука, не формат медиатреков. Только uasync; raw publisher начинает с 0. int stream_publish_set_audio_enabled(struct STREAM_HANDLE *handle, int enabled); + /* Параметры берутся из подписанного профиля источника; дубликаты локальных владельцев делят один буфер. * WAIT — источник ещё не объявил/не подтвердил трансляцию. Offline не создаёт ложного онлайн-кандидата. */ int stream_subscribe_open(struct UTUN_INSTANCE *inst, uint64_t group_id, uint64_t source_id, struct STREAM_HANDLE **out); -/* Ручные выдачи для диагностики/явного управления; режим общий для всех owners экземпляра и не смешивается с automatic. */ + +// Ручные выдачи для диагностики/явного управления; режим общий для всех owners экземпляра и не смешивается с automatic. int stream_subscribe_manual_open(struct UTUN_INSTANCE *inst, uint64_t group_id, uint64_t source_id, struct STREAM_HANDLE **out); -/* По умолчанию: подтверждение улучшения 2000 мс, удержание 5000 мс, гистерезис интегральной оценки 20 мс. */ + +// ── Выбор поставщиков и состояние handles ── + +// По умолчанию: подтверждение улучшения 2000 мс, удержание 5000 мс, гистерезис интегральной оценки 20 мс. int stream_selection_configure(struct STREAM_HANDLE *handle, const struct stream_selection_settings *settings); + +// Скопировать статистику выбора; для сессии без автоматического планировщика — нули. STREAM_OK/STREAM_ERR_STATE. int stream_selection_get_info(const struct STREAM_HANDLE *handle, struct stream_selection_info *info); + +// Освободить свой handle, в том числе после CLOSED/удаления группы; NULL допустим. После close указатель недействителен. void stream_close(struct STREAM_HANDLE *handle); + +// 1 — handle ещё связан с рабочей сессией; 0 — NULL или отсоединён. Не проверяет наличие свежих данных. int stream_handle_active(const struct STREAM_HANDLE *handle); + +// Причина отсоединения из stream_close_reason; для NULL — STREAM_CLOSE_STOPPED. int stream_handle_reason(const struct STREAM_HANDLE *handle); + +// Скопировать состояние общей сессии и кольца; STREAM_OK или STREAM_ERR_STATE. int stream_get_info(const struct STREAM_HANDLE *handle, struct stream_info *info); +// ── Чтение аудио/видео ── + /* Медиаплеер использует тот же RS-buffer, но собственные позиции чтения. * tracks=0 включает только общий индекс; VIDEO или VIDEO|AUDIO создаёт cursor этого handle. * Все вызовы — uasync. Заимствованный packet действует до следующего read/close. * LIVE DATA/GAP/END callbacks описывают сетевой прогресс; конец отложенного просмотра получает media_read. */ int stream_media_enable(struct STREAM_HANDLE *handle, unsigned tracks); + /* Атомарно открыть viewer выбранной эпохи с собственным cursor. CONFLICT означает смену источника. * tracks=VIDEO или VIDEO|AUDIO; out заранее NULL, при отказе остаётся NULL. Закрытие через stream_close. */ int stream_media_open(struct UTUN_INSTANCE *inst, uint64_t group_id, uint64_t source_id, uint64_t epoch, unsigned tracks, struct STREAM_HANDLE **out); -/* Один следующий видеокадр при want_video=1; звук ограничен position_us+audio_ahead_us. */ + +/* Прочитать ближайшую медиазапись: want_video=1 разрешает видео, звук ограничен position_us+audio_ahead_us. + * STREAM_OK — packet заполнен, его kind также сообщает конец трека/потока; WAIT/MORE — повторить позже. + * GAP/EXPIRED требуют новой точки KEY через resync; отрицательный результат — ошибка. Буфер packet заимствован. */ int stream_media_read(struct STREAM_HANDLE *handle, int64_t position_us, int want_video, int64_t audio_ahead_us, struct stream_media_packet *packet); + +// Снимок курсора и доступных медиаданных; STREAM_OK или отрицательная ошибка. int stream_media_get_status(struct STREAM_HANDLE *handle, struct stream_media_status *status); + +// Сбросить только свой медиакурсор для поиска новой точки KEY; STREAM_OK или STREAM_ERR_STATE. int stream_media_resync(struct STREAM_HANDLE *handle); +// ── Публикация байтового потока ── + +// Максимальная порция одного write/block в байтах; 0 — неактивный handle или не источник. size_t stream_publish_capacity(const struct STREAM_HANDLE *handle); + +/* Скопировать length <= stream_publish_capacity(handle) байт в источник; accepted обязателен даже при ошибке. + * Входной буфер остаётся у вызывающего. STREAM_OK — принято; отрицательный результат — ошибка. */ int stream_publish_write(struct STREAM_HANDLE *handle, const uint8_t *data, size_t length, size_t *accepted); -/* Публикует одну непустую FEC-пачку, без склейки/таймера. Не смешивать с publish_write в одном запуске. */ + +/* Скопировать одну непустую порцию размером <= capacity как отдельную FEC-пачку; STREAM_OK или отрицательная ошибка. + * Входной буфер остаётся у вызывающего. Не смешивать с publish_write в одном запуске. */ int stream_publish_block(struct STREAM_HANDLE *handle, const uint8_t *data, size_t length); -/* Сбросить остаток, снять статус публикации; DATA перед END. Буфер живёт до close. */ + +// Сбросить остаток, снять статус публикации; DATA перед END. Буфер живёт до close. int stream_publish_finish(struct STREAM_HANDLE *handle); +// ── Кандидаты для сетевых выдач ── + /* Только подходящие online-подписчики этой группы/эпохи; мобильные relay исключены, источник допускается. * out заранее NULL; массив освобождается u_free(). Наличие кандидата не гарантирует прямой транспорт. */ int stream_get_candidates(const struct STREAM_HANDLE *handle, struct stream_candidate **out, size_t *count); diff --git a/src/stream/stream.h1 b/src/stream/stream.h1 new file mode 100644 index 00000000..51227c9e --- /dev/null +++ b/src/stream/stream.h1 @@ -0,0 +1,125 @@ +/* Стриминг CHAT-сервиса: локальные handles владеют общим экземпляром трансляции. + * Все API — uasync-поток. Последний reader close освобождает экземпляр; publisher close после FIN + * сохраняет кольцо для локальных readers, без FIN отсоединяет их. Отсоединённый handle также закрывает вызывающий. + * DATA/GAP/END/CLOSED/AVAILABLE callback отложен, data/описание заимствованы до его возврата. + * Close handles из callback допустим; остановка сервиса/группы и destroy ядра — вне callbacks. + * Источник и локальные подписки публикуются атомарно через опции собственного мембера. */ +#ifndef STREAM_H +#define STREAM_H + +#include "stream_buffer.h" +#include "stream_media_buffer.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct UTUN_INSTANCE; +struct STREAM_HANDLE; +struct stream_service; + +struct stream_parameters { uint8_t n, r; uint16_t block_size; }; /* block_size — максимум RS-части. */ + +enum stream_event_type { STREAM_EVENT_DATA, STREAM_EVENT_GAP, STREAM_EVENT_END, STREAM_EVENT_CLOSED, STREAM_EVENT_AVAILABLE }; +enum stream_close_reason { + STREAM_CLOSE_STOPPED, STREAM_CLOSE_GROUP, STREAM_CLOSE_SERVICE, STREAM_CLOSE_SOURCE_CHANGED, + STREAM_CLOSE_MEMBER, STREAM_CLOSE_FAILED +}; + +struct stream_event { + struct stream_descriptor descriptor; + enum stream_event_type type; + uint64_t first_seq, last_seq; + const uint8_t *data; + size_t length; + int reason; +}; +typedef void (*stream_event_fn)(struct UTUN_INSTANCE *inst, const struct stream_event *event, void *arg); + +struct stream_info { + struct stream_descriptor descriptor; + struct stream_buffer_stats buffer; + unsigned owners; + int publisher; + int finished; +}; + +struct stream_candidate { + uint64_t node_id; + uint8_t client_type; + uint8_t source; +}; + +struct stream_selection_settings { uint32_t migration_confirm_ms, minimum_hold_ms, score_hysteresis_ms; }; +struct stream_selection_info { + uint64_t probes_started, cycles, migrations, failures, restarts; + unsigned candidates, probes_active, subflows, overlaps, providers; + int automatic, protected_by_fec; +}; + +int stream_service_init(struct UTUN_INSTANCE *inst); +void stream_service_destroy(struct UTUN_INSTANCE *inst); +void stream_cancel_group(struct UTUN_INSTANCE *inst, uint64_t group_id); +void stream_set_event_callback(struct UTUN_INSTANCE *inst, stream_event_fn callback, void *arg); +/* Проверенные online-публикации: JSON с id(decimal string), epoch(hex string), name, self. + * Только uasync; *out заранее NULL, результат освобождается u_free(). WAIT — группа недоступна. */ +int stream_sources_json(struct UTUN_INSTANCE *inst, uint64_t group_id, char **out); +/* Независимое наблюдение owner: начальный снимок и изменения через CHAT_EVT_STREAM_SOURCES. + * group_id=0 снимает только этого owner; owner=0 разрешён для выбранного канала frontend. + * Недоступный каталог имеет valid=0 и не означает завершение источников. + * Не создаёт подписку/буфер и не удерживает ACTIVE. Только uasync. */ +int stream_watch_sources(struct UTUN_INSTANCE *inst, uint64_t group_id, uint64_t owner); +/* Локальный owner удерживает эффективный ACTIVE; желаемый frontend-режим ядро восстанавливает после последнего close. */ +int stream_service_needs_activity(const struct UTUN_INSTANCE *inst); +/* Только локальный viewer этой группы/эпохи подавляет рацию источника со включённым звуком. */ +int stream_radio_muted(struct UTUN_INSTANCE *inst, uint64_t group_id, uint64_t source_id); + +/* out заранее NULL. parameters=NULL — 6+2, часть до 1300 байт и одна подпись пачки внутри RS. + * Одна публикация этого узла в группе; новый запуск получает новый source_epoch. */ +int stream_publish_open(struct UTUN_INSTANCE *inst, uint64_t group_id, const struct stream_parameters *parameters, + struct STREAM_HANDLE **out); +/* Меняет подписанное состояние звука, не формат медиатреков. Только uasync; raw publisher начинает с 0. */ +int stream_publish_set_audio_enabled(struct STREAM_HANDLE *handle, int enabled); +/* Параметры берутся из подписанного профиля источника; дубликаты локальных владельцев делят один буфер. + * WAIT — источник ещё не объявил/не подтвердил трансляцию. Offline не создаёт ложного онлайн-кандидата. */ +int stream_subscribe_open(struct UTUN_INSTANCE *inst, uint64_t group_id, uint64_t source_id, struct STREAM_HANDLE **out); +/* Ручные выдачи для диагностики/явного управления; режим общий для всех owners экземпляра и не смешивается с automatic. */ +int stream_subscribe_manual_open(struct UTUN_INSTANCE *inst, uint64_t group_id, uint64_t source_id, struct STREAM_HANDLE **out); +/* По умолчанию: подтверждение улучшения 2000 мс, удержание 5000 мс, гистерезис интегральной оценки 20 мс. */ +int stream_selection_configure(struct STREAM_HANDLE *handle, const struct stream_selection_settings *settings); +int stream_selection_get_info(const struct STREAM_HANDLE *handle, struct stream_selection_info *info); +void stream_close(struct STREAM_HANDLE *handle); +int stream_handle_active(const struct STREAM_HANDLE *handle); +int stream_handle_reason(const struct STREAM_HANDLE *handle); +int stream_get_info(const struct STREAM_HANDLE *handle, struct stream_info *info); + +/* Медиаплеер использует тот же RS-buffer, но собственные позиции чтения. + * tracks=0 включает только общий индекс; VIDEO или VIDEO|AUDIO создаёт cursor этого handle. + * Все вызовы — uasync. Заимствованный packet действует до следующего read/close. + * LIVE DATA/GAP/END callbacks описывают сетевой прогресс; конец отложенного просмотра получает media_read. */ +int stream_media_enable(struct STREAM_HANDLE *handle, unsigned tracks); +/* Атомарно открыть viewer выбранной эпохи с собственным cursor. CONFLICT означает смену источника. + * tracks=VIDEO или VIDEO|AUDIO; out заранее NULL, при отказе остаётся NULL. Закрытие через stream_close. */ +int stream_media_open(struct UTUN_INSTANCE *inst, uint64_t group_id, uint64_t source_id, uint64_t epoch, + unsigned tracks, struct STREAM_HANDLE **out); +/* Один следующий видеокадр при want_video=1; звук ограничен position_us+audio_ahead_us. */ +int stream_media_read(struct STREAM_HANDLE *handle, int64_t position_us, int want_video, int64_t audio_ahead_us, + struct stream_media_packet *packet); +int stream_media_get_status(struct STREAM_HANDLE *handle, struct stream_media_status *status); +int stream_media_resync(struct STREAM_HANDLE *handle); + +size_t stream_publish_capacity(const struct STREAM_HANDLE *handle); +int stream_publish_write(struct STREAM_HANDLE *handle, const uint8_t *data, size_t length, size_t *accepted); +/* Публикует одну непустую FEC-пачку, без склейки/таймера. Не смешивать с publish_write в одном запуске. */ +int stream_publish_block(struct STREAM_HANDLE *handle, const uint8_t *data, size_t length); +/* Сбросить остаток, снять статус публикации; DATA перед END. Буфер живёт до close. */ +int stream_publish_finish(struct STREAM_HANDLE *handle); + +/* Только подходящие online-подписчики этой группы/эпохи; мобильные relay исключены, источник допускается. + * out заранее NULL; массив освобождается u_free(). Наличие кандидата не гарантирует прямой транспорт. */ +int stream_get_candidates(const struct STREAM_HANDLE *handle, struct stream_candidate **out, size_t *count); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/stream/stream_optimizer.h b/src/stream/stream_optimizer.h index cc11156b..e891a4de 100644 --- a/src/stream/stream_optimizer.h +++ b/src/stream/stream_optimizer.h @@ -1,5 +1,7 @@ -/* Один планировщик на локальный экземпляр; handles владельцев не размножают пробы/выдачи. - * tick вызывается отложенным stream network проходом и таймером, destroy отменяет все свои receive/probe handles. */ +/* stream_optimizer — автоматический выбор узлов, от которых получать части трансляции. + * Проверяет поставщиков, сравнивает задержку и переносит выдачи на лучший доступный путь; при необходимости просит relay. + * Внутренний модуль stream: create для одной трансляции → tick при сетевом событии/по таймеру → destroy. + * Все вызовы — uasync. Планировщик общий для локальных читателей; destroy отменяет принадлежащие ему пробы и выдачи. */ #ifndef STREAM_OPTIMIZER_H #define STREAM_OPTIMIZER_H @@ -7,20 +9,42 @@ struct stream_optimizer; struct stream_receive_info; -/* Меньшая оценка лучше: chain RTT + 2 * peer RTT + 3 мс/hop + штраф 0..1000 мс; неизвестный RTT = UINT64_MAX. */ + +// ── Оценка поставщиков и расписание проб ── + +// Меньшая оценка лучше: chain RTT + 2 * peer RTT + 3 мс/hop + штраф 0..1000 мс; неизвестный RTT = UINT64_MAX. uint64_t stream_supplier_score(const struct stream_receive_info *info, const struct stream_metric_snapshot *metrics, int valid); + /* Только монотонное расписание; pending/active — незавершённый обход/пробы текущего цикла. * Возвращает допустимое число новых узлов, не адресов/субпотоков: 32 один раз, затем 0/1. */ struct stream_probe_schedule { uint64_t cycle, started_ms, next_ms; }; unsigned stream_probe_schedule_tick(struct stream_probe_schedule *schedule, uint64_t now_ms, unsigned pending, unsigned active); + +// ── Жизненный цикл планировщика ── + +// Скопировать descriptor и создать планировщик; *out заранее NULL. STREAM_OK или отрицательная ошибка. int stream_optimizer_create(struct UTUN_INSTANCE *inst, const struct stream_descriptor *descriptor, struct stream_optimizer **out); + +// Остановить пробы/выдачи и освободить планировщик; NULL допустим, instance ещё должен быть жив. void stream_optimizer_destroy(struct stream_optimizer *optimizer); + +// ── Выбор поставщиков и настройки ── + +// Выполнить очередной проход выбора для живой подписки; now_ms — монотонное время в миллисекундах. void stream_optimizer_tick(struct stream_optimizer *optimizer, struct STREAM_HANDLE *subscription, uint64_t now_ms); + +// Скопировать пороги подтверждения, удержания и гистерезиса в мс (каждый <=60000); STREAM_OK/STREAM_ERR_ARGUMENT. int stream_optimizer_configure(struct stream_optimizer *optimizer, const struct stream_selection_settings *settings); + +// Скопировать статистику; optimizer и info обязательны. void stream_optimizer_info(struct stream_optimizer *optimizer, struct stream_selection_info *info); -/* Merkle/BGP помечает список поставщиков устаревшим; пробы/цикл/backoff сохраняются до отложенной сверки. */ + +// ── Уведомления от Merkle/BGP и relay ── + +// Merkle/BGP помечает список поставщиков устаревшим; пробы/цикл/backoff сохраняются до отложенной сверки. void stream_optimizer_roster_changed(struct stream_optimizer *optimizer, uint64_t peer); -/* Подтверждённая помощь немедленно обновляет кандидатов и снимает старый backoff конкретного пира. */ + +// Подтверждённая помощь немедленно обновляет кандидатов и снимает старый backoff конкретного пира. void stream_optimizer_refresh(struct stream_optimizer *optimizer, uint64_t peer); #endif diff --git a/src/stream/stream_optimizer.h1 b/src/stream/stream_optimizer.h1 new file mode 100644 index 00000000..cc11156b --- /dev/null +++ b/src/stream/stream_optimizer.h1 @@ -0,0 +1,26 @@ +/* Один планировщик на локальный экземпляр; handles владельцев не размножают пробы/выдачи. + * tick вызывается отложенным stream network проходом и таймером, destroy отменяет все свои receive/probe handles. */ +#ifndef STREAM_OPTIMIZER_H +#define STREAM_OPTIMIZER_H + +#include "stream.h" + +struct stream_optimizer; +struct stream_receive_info; +/* Меньшая оценка лучше: chain RTT + 2 * peer RTT + 3 мс/hop + штраф 0..1000 мс; неизвестный RTT = UINT64_MAX. */ +uint64_t stream_supplier_score(const struct stream_receive_info *info, const struct stream_metric_snapshot *metrics, int valid); +/* Только монотонное расписание; pending/active — незавершённый обход/пробы текущего цикла. + * Возвращает допустимое число новых узлов, не адресов/субпотоков: 32 один раз, затем 0/1. */ +struct stream_probe_schedule { uint64_t cycle, started_ms, next_ms; }; +unsigned stream_probe_schedule_tick(struct stream_probe_schedule *schedule, uint64_t now_ms, unsigned pending, unsigned active); +int stream_optimizer_create(struct UTUN_INSTANCE *inst, const struct stream_descriptor *descriptor, struct stream_optimizer **out); +void stream_optimizer_destroy(struct stream_optimizer *optimizer); +void stream_optimizer_tick(struct stream_optimizer *optimizer, struct STREAM_HANDLE *subscription, uint64_t now_ms); +int stream_optimizer_configure(struct stream_optimizer *optimizer, const struct stream_selection_settings *settings); +void stream_optimizer_info(struct stream_optimizer *optimizer, struct stream_selection_info *info); +/* Merkle/BGP помечает список поставщиков устаревшим; пробы/цикл/backoff сохраняются до отложенной сверки. */ +void stream_optimizer_roster_changed(struct stream_optimizer *optimizer, uint64_t peer); +/* Подтверждённая помощь немедленно обновляет кандидатов и снимает старый backoff конкретного пира. */ +void stream_optimizer_refresh(struct stream_optimizer *optimizer, uint64_t peer); + +#endif diff --git a/src/stream/stream_playback_clock.h b/src/stream/stream_playback_clock.h index 531dfa34..09236efb 100644 --- a/src/stream/stream_playback_clock.h +++ b/src/stream/stream_playback_clock.h @@ -1,11 +1,13 @@ -/* Общая адаптация воспроизведения desktop/Android, без сетевых ресурсов и очередей медиа. - * Один поток либо внешняя блокировка. position_us — фактические общие часы исходного материала; - * ready_us — непрерывная готовая граница, включая данные в кольце/кодеке/устройстве. +/* stream_playback_clock — выбор темпа воспроизведения для плавного сокращения задержки live-трансляции. + * Reset начинает новую шкалу; update получает проигранную позицию и границу готовых данных, возвращает темп и запас. + * Временная нехватка сохраняет историю; новая эпоха, GAP или перемотка требуют reset. + * Общий модуль desktop/Android; не владеет сетью или PCM. Один поток либо внешняя блокировка. */ + +/* Правила адаптации: * Минимумы короткого окна нормализованы по смещению wall-clock относительно media-clock: * простой увеличивает все прежние запасы, догон уменьшает их без переписывания истории. * После 200 мс без фактической недостачи избыток погашается плавным ускорением до 1.6. - * guard_us и уже поставленный устройству pending_gain_us защищают остаток при снижении темпа. - * reset нужен при новой эпохе/реальном GAP/смене позиции; временная нехватка историю сохраняет. */ + * guard_us и уже поставленный устройству pending_gain_us защищают остаток при снижении темпа. */ #ifndef STREAM_PLAYBACK_CLOCK_H #define STREAM_PLAYBACK_CLOCK_H @@ -35,8 +37,16 @@ struct stream_playback_state { int tempo_milli, waiting, history_ready; }; +// ── Управление временной шкалой ── + +// Обнулить историю и вернуть темп 1x; id — идентификатор плеера для диагностики. void stream_playback_clock_reset(struct stream_playback_clock *clock, uint64_t id); -/* Результат не меняется при ошибке. starved отмечает фактическую остановку подготовки/вывода. + +/* now_us — монотонное время, position_us — фактически проигранная позиция исходного материала; + * ready_us — непрерывная готовая граница, включая кольцо, кодек и устройство. Все времена — микросекунды. + * guard_us — неприкосновенный запас; pending_gain_us — ещё не проигранное сокращение длительности PCM в устройстве. + * starved отмечает фактическую остановку подготовки/вывода; state получает темп (tempo_milli: 1000=1x) и запас. + * Результат не меняется при ошибке. * Временное уменьшение фактического position_us запрещено; после перемотки вызывают reset. * Возврат 0 — успех, отрицательный — неверные аргументы/часы. */ int stream_playback_clock_update(struct stream_playback_clock *clock, int64_t now_us, int64_t position_us, diff --git a/src/stream/stream_playback_clock.h1 b/src/stream/stream_playback_clock.h1 new file mode 100644 index 00000000..531dfa34 --- /dev/null +++ b/src/stream/stream_playback_clock.h1 @@ -0,0 +1,49 @@ +/* Общая адаптация воспроизведения desktop/Android, без сетевых ресурсов и очередей медиа. + * Один поток либо внешняя блокировка. position_us — фактические общие часы исходного материала; + * ready_us — непрерывная готовая граница, включая данные в кольце/кодеке/устройстве. + * Минимумы короткого окна нормализованы по смещению wall-clock относительно media-clock: + * простой увеличивает все прежние запасы, догон уменьшает их без переписывания истории. + * После 200 мс без фактической недостачи избыток погашается плавным ускорением до 1.6. + * guard_us и уже поставленный устройству pending_gain_us защищают остаток при снижении темпа. + * reset нужен при новой эпохе/реальном GAP/смене позиции; временная нехватка историю сохраняет. */ +#ifndef STREAM_PLAYBACK_CLOCK_H +#define STREAM_PLAYBACK_CLOCK_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define STREAM_PLAYBACK_HISTORY 4 +#define STREAM_PLAYBACK_BUCKET_US 50000 +#define STREAM_PLAYBACK_SETTLE_US 200000 +#define STREAM_PLAYBACK_MAX_TEMPO 1600 +#define STREAM_PLAYBACK_RAMP_US 500000 + +struct stream_playback_bucket { int64_t period, minimum_us; }; +struct stream_playback_clock { + struct stream_playback_bucket history[STREAM_PLAYBACK_HISTORY]; + uint64_t id, pauses; + int64_t first_us, last_us, last_position_us, correction_us, last_bad_us, pause_us, report_us; + int64_t ramp_remainder; + int tempo_milli, initialized, waiting; +}; + +struct stream_playback_state { + int64_t position_us, reserve_us, minimum_us, correction_us, stable_us, removable_us; + int tempo_milli, waiting, history_ready; +}; + +void stream_playback_clock_reset(struct stream_playback_clock *clock, uint64_t id); +/* Результат не меняется при ошибке. starved отмечает фактическую остановку подготовки/вывода. + * Временное уменьшение фактического position_us запрещено; после перемотки вызывают reset. + * Возврат 0 — успех, отрицательный — неверные аргументы/часы. */ +int stream_playback_clock_update(struct stream_playback_clock *clock, int64_t now_us, int64_t position_us, + int64_t ready_us, int64_t guard_us, int64_t pending_gain_us, + int starved, struct stream_playback_state *state); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/transport_layer/auto_socket.h b/src/transport_layer/auto_socket.h index d97d4171..3775e69d 100644 --- a/src/transport_layer/auto_socket.h +++ b/src/transport_layer/auto_socket.h @@ -1,3 +1,9 @@ +/* auto_socket — создание UDP/TCP-сокетов и линков при появлении сетевых интерфейсов. + * При auto_sockets=yes init сканирует интерфейсы и включает мониторинг Linux/BSD/Windows; + * адреса и набор сокетов затем обновляются автоматически. Порты сохраняются в SQLite для повторного использования. + * Destroy отключает мониторинг и удаляет созданные ресурсы. В режиме android сокеты приходят из конфигурации frontend. + * Основной API — uasync-поток. Из другого потока вызывать только on_network_change, пока instance жив. + * Add/remove_interface предназначены для платформенного мониторинга; смена сети сама не создаёт членство в группе. */ #ifndef AUTO_SOCKET_H #define AUTO_SOCKET_H @@ -5,35 +11,6 @@ extern "C" { #endif -/* - * auto_socket.h — автоматическое управление сокетами при изменении сетевых интерфейсов - * - * === Зачем нужен модуль === - * При auto_sockets=yes не нужно вручную прописывать секции [server] в конфиге. - * Модуль сам создаёт UDP+TCP сокеты на всех не-loopback интерфейсах с IP-адресами, - * и сразу добавляет линки ко всем активным ETCP-соединениям. При пропадании - * интерфейса линки удаляются, сокеты закрываются. - * - * === Что делает === - * - Для каждого интерфейса (UP, с v4/v6 адресами) создаёт пару сокетов: - * UDP (случайный порт) + TCP (stcp_server на случайном порту) - * - Тип сокета: PUBLIC если на интерфейсе есть глобальный адрес, иначе NAT - * - Порт сохраняется в SQLite для переиспользования при перезапуске - * - Для каждого нового сокета: etcp_link_new ко всем соединениям (inst->connections), - * случайно выбирая один совместимый адрес пира (отдельно для v4 и v6) - * - При изменении адреса на интерфейсе — обновляет interface_addr и оповещает - * подписчиков (ETCP_SOCKET_EVENT_ADDR_CHANGED) - * - При смене типа (NAT↔PUBLIC) — пересоздаёт сокет с новым типом - * - При пропадании интерфейса — удаляет линки, закрывает сокет, чистит БД - * - * === Слои === - * 1. Platform-agnostic API: auto_socket_init / auto_socket_destroy - * auto_socket_add_interface / auto_socket_remove_interface - * (можно вызывать из любого платформенного мониторинга: netlink, JNI и т.д.) - * 2. Platform monitoring: встроенные обработчики netlink (Linux), - * route socket (BSD), IP Helper (Windows) — работают автоматически - */ - #include struct UTUN_INSTANCE; @@ -45,14 +22,14 @@ struct utun_config; * handle'ы для мониторинга (netlink fd, route socket fd, IP Helper handles). */ struct AUTO_SOCKET; -/* ─── Жизненный цикл ─── */ +// ─── Жизненный цикл ─── /* Инициализирует модуль: создаёт таблицу портов в БД, запускает платформенный * мониторинг интерфейсов, сканирует все текущие UP-интерфейсы и создаёт сокеты. * Если auto_sockets=no в конфиге — ничего не делает, возвращает 0. */ int auto_socket_init(struct UTUN_INSTANCE* inst); -/* Останавливает мониторинг, удаляет все созданные сокеты и линки, освобождает память. */ +// Останавливает мониторинг, удаляет все созданные сокеты и линки, освобождает память. void auto_socket_destroy(struct UTUN_INSTANCE* inst); /* Вызвать при изменении сетевых интерфейсов из любого потока (JNI, ConnectivityManager). @@ -64,7 +41,7 @@ void auto_socket_on_network_change(struct UTUN_INSTANCE* inst); * inst->config, остальное освобождается. Должен вызываться в uasync-потоке. */ void auto_socket_reload_config(struct UTUN_INSTANCE* inst, struct utun_config* new_cfg); -/* ─── Platform-agnostic API: управление сокетами на конкретном интерфейсе ─── */ +// ─── Внутренний API платформенного мониторинга ─── /* * Создать UDP+TCP сокеты для интерфейса и добавить линки ко всем активным соединениям. diff --git a/src/transport_layer/auto_socket.h1 b/src/transport_layer/auto_socket.h1 new file mode 100644 index 00000000..d97d4171 --- /dev/null +++ b/src/transport_layer/auto_socket.h1 @@ -0,0 +1,95 @@ +#ifndef AUTO_SOCKET_H +#define AUTO_SOCKET_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * auto_socket.h — автоматическое управление сокетами при изменении сетевых интерфейсов + * + * === Зачем нужен модуль === + * При auto_sockets=yes не нужно вручную прописывать секции [server] в конфиге. + * Модуль сам создаёт UDP+TCP сокеты на всех не-loopback интерфейсах с IP-адресами, + * и сразу добавляет линки ко всем активным ETCP-соединениям. При пропадании + * интерфейса линки удаляются, сокеты закрываются. + * + * === Что делает === + * - Для каждого интерфейса (UP, с v4/v6 адресами) создаёт пару сокетов: + * UDP (случайный порт) + TCP (stcp_server на случайном порту) + * - Тип сокета: PUBLIC если на интерфейсе есть глобальный адрес, иначе NAT + * - Порт сохраняется в SQLite для переиспользования при перезапуске + * - Для каждого нового сокета: etcp_link_new ко всем соединениям (inst->connections), + * случайно выбирая один совместимый адрес пира (отдельно для v4 и v6) + * - При изменении адреса на интерфейсе — обновляет interface_addr и оповещает + * подписчиков (ETCP_SOCKET_EVENT_ADDR_CHANGED) + * - При смене типа (NAT↔PUBLIC) — пересоздаёт сокет с новым типом + * - При пропадании интерфейса — удаляет линки, закрывает сокет, чистит БД + * + * === Слои === + * 1. Platform-agnostic API: auto_socket_init / auto_socket_destroy + * auto_socket_add_interface / auto_socket_remove_interface + * (можно вызывать из любого платформенного мониторинга: netlink, JNI и т.д.) + * 2. Platform monitoring: встроенные обработчики netlink (Linux), + * route socket (BSD), IP Helper (Windows) — работают автоматически + */ + +#include + +struct UTUN_INSTANCE; +struct ETCP_SOCKET; +struct utun_config; + +/* Внутреннее состояние модуля — хранится в inst->auto_socket_state. + * Содержит связный список отслеживаемых интерфейсов (ifaces) и платформенные + * handle'ы для мониторинга (netlink fd, route socket fd, IP Helper handles). */ +struct AUTO_SOCKET; + +/* ─── Жизненный цикл ─── */ + +/* Инициализирует модуль: создаёт таблицу портов в БД, запускает платформенный + * мониторинг интерфейсов, сканирует все текущие UP-интерфейсы и создаёт сокеты. + * Если auto_sockets=no в конфиге — ничего не делает, возвращает 0. */ +int auto_socket_init(struct UTUN_INSTANCE* inst); + +/* Останавливает мониторинг, удаляет все созданные сокеты и линки, освобождает память. */ +void auto_socket_destroy(struct UTUN_INSTANCE* inst); + +/* Вызвать при изменении сетевых интерфейсов из любого потока (JNI, ConnectivityManager). + * Откладывает полное сканирование (auto_socket_scan_all) в uasync-поток. */ +void auto_socket_on_network_change(struct UTUN_INSTANCE* inst); + +/* Android-режим (auto_sockets=android): обновить сокеты из [server] секций нового + * конфига. new_cfg — уже распарсенный конфиг; его список servers переносится в + * inst->config, остальное освобождается. Должен вызываться в uasync-потоке. */ +void auto_socket_reload_config(struct UTUN_INSTANCE* inst, struct utun_config* new_cfg); + +/* ─── Platform-agnostic API: управление сокетами на конкретном интерфейсе ─── */ + +/* + * Создать UDP+TCP сокеты для интерфейса и добавить линки ко всем активным соединениям. + * ifindex — индекс интерфейса (0 = автоопределение по ifname) + * ifname — имя интерфейса для SO_BINDTODEVICE + * has_v4 — есть ли IPv4 адреса на интерфейсе + * has_v6 — есть ли IPv6 адреса на интерфейсе + * v4_type — CFG_SERVER_TYPE_PUBLIC или CFG_SERVER_TYPE_NAT + * v6_type — CFG_SERVER_TYPE_PUBLIC или CFG_SERVER_TYPE_NAT + * Возвращает количество созданных сокетов (0, 1, 2, 3 или 4 — до 4: v4 UDP+TCP, v6 UDP+TCP). + * Если сокеты для этого интерфейса уже есть — ничего не делает, возвращает 0. + */ +int auto_socket_add_interface(struct UTUN_INSTANCE* inst, uint32_t ifindex, + const char* ifname, int has_v4, int has_v6, + uint8_t v4_type, uint8_t v6_type); + +/* + * Удалить все сокеты и линки для интерфейса. + * Закрывает UDP/TCP сокеты, удаляет etcp-линки через ncd_remove_socket_links, + * чистит запись порта в БД. + * ifindex — индекс интерфейса, сокеты которого удаляем + */ +void auto_socket_remove_interface(struct UTUN_INSTANCE* inst, uint32_t ifindex); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/transport_layer/etcp_connections.h b/src/transport_layer/etcp_connections.h index 39f8cb83..0ec1d312 100644 --- a/src/transport_layer/etcp_connections.h +++ b/src/transport_layer/etcp_connections.h @@ -1,3 +1,9 @@ +/* etcp_connections — сокеты и физические линки прямого ETCP-соединения с пиром. + * Принимает/отправляет пакеты, выполняет INIT, PING и подключает UDP/STCP-линки к общему ETCP_CONN. + * Ядро создаёт сокеты через init_sockets/init_connections; NCD управляет линками к конкретным узлам. + * Сервисы открывают транспорт через node_conn_direct.h, обмениваются данными через etcp_api.h или etcp_router.h. + * Этот заголовок содержит внутренние структуры и API стека. Все операции — uasync-поток instance. + * Сокет обслуживает несколько пиров; линк соединяет один сокет с одним адресом пира. */ #ifndef ETCP_CONNECTIONS_H #define ETCP_CONNECTIONS_H @@ -6,8 +12,6 @@ extern "C" { #endif -// подмодуль ETCP который обслуживает сокеты ETCP для приёма-передачи пакетов и одно ETCP подключение через несколько каналов связи (failover) - #include "secure_channel.h" #include "etcp_session.h" #include "reality.h" @@ -52,7 +56,7 @@ extern "C" { #define ETCP_PING_FLAG_ADDRESS_CHECK 0x08 // прямое входящее испытание endpoint #define ETCP_PING_FLAG_NO_REPLY 0x10 // NAT-проба: результат через router, без автоматического UDP-ответа -/* Адаптивный keepalive */ +// Адаптивный keepalive #define KA_PERIOD_MIN_MS 200 #define KA_PERIOD_MAX_MS 10000 #define KA_TIMEOUT_MULT 10 /* timeout = period * mult */ @@ -69,10 +73,10 @@ struct ETCP_DGRAM {// пакет (незашифрованный) }; #pragma pack(pop) -/* Размер зашифрованного заголовка ETCP_DGRAM (timestamp + flag_up + padding) */ +// Размер зашифрованного заголовка ETCP_DGRAM (timestamp + flag_up + padding) #define ETCP_ENCRYPTED_HDR_SIZE (offsetof(struct ETCP_DGRAM, data) - offsetof(struct ETCP_DGRAM, timestamp)) -/* Размер базового ACK: type(1) + count(1) + last_delivered_id(4) + rx_dup_count(2) */ +// Размер базового ACK: type(1) + count(1) + last_delivered_id(4) + rx_dup_count(2) #define ETCP_ACK_BASE_SIZE (2 + 4 + 2) // --- INIT packet wire formats (inside encrypted payload, before padding) --- @@ -97,6 +101,7 @@ struct ETCP_INIT_REQUEST_PKT { // V4 fields: uint8_t device_type; // 66: CLIENT_TYPE_SERVER/DESKTOP/MOBILE } __attribute__((packed)); + #define ETCP_INIT_REQ_SIZE sizeof(struct ETCP_INIT_REQUEST_PKT) // INIT RESPONSE: server → client @@ -118,10 +123,12 @@ struct ETCP_INIT_RESPONSE_PKT { uint8_t device_type; // 61: CLIENT_TYPE_SERVER/DESKTOP/MOBILE uint8_t keepalive[2]; // 62: server keepalive interval (big-endian) } __attribute__((packed)); + #define ETCP_INIT_RESP_SIZE sizeof(struct ETCP_INIT_RESPONSE_PKT) typedef void (*etcp_ping_callback_t)(int success, uint16_t rtt, void* arg, uint64_t nonce, const uint8_t* resp_data, size_t resp_data_len); + struct PING_CONTEXT { struct PING_CONTEXT* next; struct UTUN_INSTANCE* instance; @@ -165,10 +172,10 @@ struct ETCP_SOCKET { struct sockaddr_storage interface_addr; struct sockaddr_storage nat_addr; - /* ── uasync handle (UDP read_cb / TCP accept_cb) ── */ + // ── uasync handle (UDP read_cb / TCP accept_cb) ── void* socket_id; - /* ── UDP-only ── */ + // ── UDP-only ── struct ll_queue* links_queue; // hash-очередь линков int errorcode; size_t pkt_format_errors; @@ -332,27 +339,34 @@ struct ETCP_LINK { uint32_t tcp_reconnect_delay_ms; // задержка реконнекта (экспоненциальный backoff) }; +// ── Внутренняя отправка пакетов ── + // Единая точка отправки всех ETCP-пакетов. ssize_t etcp_udp_send(struct ETCP_LINK* link, socket_t fd, const void* buf, size_t len, const struct sockaddr* addr, socklen_t addr_len); -// INITIALIZATION -// Создаёт только listen-сокеты из конфига (серверы для incoming connections) +// ── Запуск транспорта ядра ── + +// Создать listen-сокеты из [server]; при auto_sockets их создаёт отдельный модуль. int init_sockets(struct UTUN_INSTANCE* instance); -// Создаёт listen-сокеты и client connections из конфига + +// Подготовить listen-сокеты, NCD control и keepalive; исходящие запросы UTUN запускаются отдельно. 0/-1. int init_connections(struct UTUN_INSTANCE* instance); -// Заменяет принадлежащие конфигурации NCD handles. Новые приобретаются до освобождения старых. +// Заменить запросы к пирам конфигурации работающей UTUN-группы. Новые открываются до закрытия старых; 0/-1. int init_client_connections(struct UTUN_INSTANCE* instance, struct CFG_CLIENT* clients); -// SOCKET FUNCTIONS -// добавляет новый версер (сокет для приёма и отправки кодограмм. обслуживает много подключений) +// ── Сокеты UDP и TCP ── + +// Создать UDP-сокет приёма/отправки из server; один сокет обслуживает линки нескольких пиров. struct ETCP_SOCKET* etcp_socket_add(struct UTUN_INSTANCE* instance, struct CFG_SERVER* server); -// удаляет сокет и освобождает ресурсы (грохает все его подключения и сокет) + +// Закрыть UDP-сокет и все привязанные к нему линки, освободить ETCP_SOCKET. void etcp_socket_remove(struct ETCP_SOCKET* conn); // TCP socket functions (uses unified ETCP_SOCKET with is_tcp=1) struct ETCP_SOCKET* tcp_socket_add(struct UTUN_INSTANCE* instance, struct CFG_SERVER* server); + void tcp_socket_remove(struct ETCP_SOCKET* sock); // Создаёт один серверный сокет (UDP или TCP) из CFG_SERVER. Линки не добавляет. @@ -362,14 +376,19 @@ struct stcp_link; struct stcp_client; void tcp_server_on_link(struct stcp_link *link, struct ETCP_SOCKET *tcp_sock); -// connection functions -// создает новый канал связи для etcp подключения (ETCP_CONN) +// ── Создание, поиск и закрытие линков ── + +// Создать линк между сокетом и адресом пира для ETCP_CONN. struct ETCP_LINK* etcp_link_new(struct ETCP_CONN* etcp, struct ETCP_SOCKET* conn, const struct sockaddr_storage* remote_addr, uint8_t is_server); + void etcp_link_update_inflight_lim(struct ETCP_LINK* link, uint32_t new_lim); + void etcp_link_close(struct ETCP_LINK* link); + //int etcp_input_cbk(struct packet_buffer* pkt, struct ETCP_SOCKET* conn);// получает расшифрованный пакет int etcp_encrypt_send(struct ETCP_DGRAM* dgram);// зашифровывает и отправляет пакет + // find link by address struct ETCP_LINK* etcp_link_find_by_addr(struct ETCP_SOCKET* e_sock, const struct sockaddr_storage* addr, int is_tcp); @@ -383,20 +402,30 @@ struct ETCP_LINK* etcp_select_collision_link(struct ETCP_CONN* conn, struct ETCP // returns first free id (0-255) or -1 if all occupied int etcp_find_free_local_link_id(struct ETCP_CONN* etcp); -// Burst measurement functions +// ── Внутренние измерения пропускной способности ── void etcp_link_burst_start(struct ETCP_LINK* link); + void etcp_link_burst_check(struct ETCP_LINK* link); + void etcp_link_burst_finish(struct ETCP_LINK* link); +// ── Внутренние переходы состояния и приём ── + void etcp_link_enter_init(struct ETCP_LINK *link); + void etcp_link_enter_reinit(struct ETCP_LINK *link); + void etcp_link_enter_ready_tcp(struct ETCP_LINK *link); + void etcp_tcp_link_start_connect(struct ETCP_LINK *link, struct sockaddr_storage *addr, uint16_t port); + void etcp_tcp_link_start_reconnect(struct ETCP_LINK *link); int etcp_packet_decrypted(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, struct ETCP_LINK* link, size_t pkt_len); +// ── Пробы доступности и RTT ── + int etcp_send_ping(struct UTUN_INSTANCE* instance, const uint8_t* peer_pubkey_bin, const struct sockaddr_storage* addr, int timeout_ms, etcp_ping_callback_t cb, void* user_arg, @@ -410,7 +439,7 @@ int etcp_send_ping_to_socket(struct UTUN_INSTANCE* instance, struct ETCP_SOCKET* const uint8_t* user_data, size_t user_data_len, uint8_t flags); -/* Отмена без callback: вызывающий владеет arg и освобождает его после отмены. */ +// Отмена без callback: вызывающий владеет arg и освобождает его после отмены. void etcp_cancel_pings(struct UTUN_INSTANCE *instance, etcp_ping_callback_t callback, void *arg); // TCP-ping: подключение к TCP-адресу peer + хендшейк с флагом PING. RTT меряется @@ -421,6 +450,8 @@ int etcp_send_tcp_ping(struct UTUN_INSTANCE* instance, const uint8_t* peer_pubke etcp_ping_callback_t cb, void* user_arg, struct stcp_client** out_cli, const struct reality_client_config *reality); +// ── Внутренний callback uasync ── + void etcp_connections_read_callback_socket(socket_t sock, void* arg); diff --git a/src/transport_layer/etcp_connections.h1 b/src/transport_layer/etcp_connections.h1 new file mode 100644 index 00000000..39f8cb83 --- /dev/null +++ b/src/transport_layer/etcp_connections.h1 @@ -0,0 +1,430 @@ +#ifndef ETCP_CONNECTIONS_H +#define ETCP_CONNECTIONS_H + +#ifdef __cplusplus +extern "C" { +#endif + + +// подмодуль ETCP который обслуживает сокеты ETCP для приёма-передачи пакетов и одно ETCP подключение через несколько каналов связи (failover) + +#include "secure_channel.h" +#include "etcp_session.h" +#include "reality.h" +#include "utun_instance.h" +#include "etcp_bbr.h" +#include "../routing_layer/address_profile.h" +#include "../lib/socket_compat.h" +#include "../lib/ll_queue.h" +#include +#include + +#define IP_UDP_OVERHEAD_V4 (20 + 8) // IP(20)+UDP(8) +#define IP_UDP_OVERHEAD_V6 (40 + 8) // IP(40)+UDP(8) +#define SC_ENCRYPT_OVERHEAD (ETCP_ENCRYPTED_HDR_SIZE + ETCP_SESSION_HEADER_SIZE + SC_NONCE_SIZE + SC_CRC32_SIZE + SC_TAG_SIZE) +#define UDP_SC_HDR_SIZE (SC_NONCE_SIZE + SC_TAG_SIZE + SC_CRC32_SIZE + 5) +#define ACK_REZERV 100// сколько байт резервировать под ack и прочие заголовки +#define WIRE_OVERHEAD(fam) ((fam) == AF_INET6 ? IP_UDP_OVERHEAD_V6 + SC_ENCRYPT_OVERHEAD : IP_UDP_OVERHEAD_V4 + SC_ENCRYPT_OVERHEAD) + +#define INFLIGHT_LIM_MIN 8192 // 8K +#define INFLIGHT_LIM_MAX 1048576 // 1M + +#define PACKET_DATA_SIZE 2048 +// Wire-MTU кап: пакет на проводе не должен превышать 1500 (согласовано с TUN_MAX_PACKET_SIZE). +// PACKET_DATA_SIZE (2048) — это внутренний буфер аллокации/приёма, а не wire-MTU. +#define PACKET_DATA_MAX_MTU 1500 +#define ETCP_MAX_PAYLOAD_SIZE (PACKET_DATA_SIZE - ETCP_ACK_BASE_SIZE - 5) /* 2035 */ +#define ETCP_RFC791_MIN_MTU 576 + +// Типы кодограмм протокола +#define ETCP_INIT_REQUEST 0x02 +#define ETCP_INIT_RESPONSE 0x03 +#define ETCP_INIT_REQUEST_NOINIT 0x04 +#define ETCP_INIT_RESPONSE_NOINIT 0x05 +#define ETCP_PING 0x06 +#define ETCP_PONG 0x07 +#define ETCP_KEEPALIVE 0x08 +#define ETCP_TCP_ADDRESS 0x0d // STCP control: code + family(4/6) + observed IP(16), без порта + +#define ETCP_PING_FLAG_WANT_RTT 0x01 // в PONG: хочу получить RTT от пингера +#define ETCP_PING_FLAG_SEND_RTT 0x02 // в PING: несу RTT для получателя +#define ETCP_PING_FLAG_OBSERVE 0x04 // PONG несёт фактически наблюдаемый IP:port +#define ETCP_PING_FLAG_ADDRESS_CHECK 0x08 // прямое входящее испытание endpoint +#define ETCP_PING_FLAG_NO_REPLY 0x10 // NAT-проба: результат через router, без автоматического UDP-ответа + +/* Адаптивный keepalive */ +#define KA_PERIOD_MIN_MS 200 +#define KA_PERIOD_MAX_MS 10000 +#define KA_TIMEOUT_MULT 10 /* timeout = period * mult */ + + +#pragma pack(push, 1) +struct ETCP_DGRAM {// пакет (незашифрованный) + struct ETCP_LINK* link;// откуда получена или куда отправялем + uint16_t data_len;// общий размер пакета не включая timestamp + uint16_t noencrypt_len;// число байт (с конца) которые не надо шифровать. для передачи pubkey + uint16_t timestamp;// timestamp отправляющего узла при отправке пакета + uint8_t flag_up:1;// bit0 = up/down (recv_keepalive) + uint8_t data[];// данные пакета (без timestamp) +}; +#pragma pack(pop) + +/* Размер зашифрованного заголовка ETCP_DGRAM (timestamp + flag_up + padding) */ +#define ETCP_ENCRYPTED_HDR_SIZE (offsetof(struct ETCP_DGRAM, data) - offsetof(struct ETCP_DGRAM, timestamp)) + +/* Размер базового ACK: type(1) + count(1) + last_delivered_id(4) + rx_dup_count(2) */ +#define ETCP_ACK_BASE_SIZE (2 + 4 + 2) + +// --- INIT packet wire formats (inside encrypted payload, before padding) --- + +// INIT REQUEST: client → server +struct ETCP_INIT_REQUEST_PKT { + uint8_t code; // 0: ETCP_INIT_REQUEST (0x02) или ETCP_INIT_REQUEST_NOINIT (0x04) + uint8_t node_id[8]; // 1: sender node_id (big-endian) + uint8_t reset_id[8]; // 9: reset epoch id (big-endian) + uint8_t mtu[2]; // 17: client MTU (big-endian) + uint8_t keepalive[2]; // 19: keepalive interval (big-endian) + uint8_t recovery[2]; // 21: recovery interval/100 (big-endian) + uint8_t link_id; // 23: client's local link id + uint8_t socket_id; // 24: client's socket id + uint8_t only_local; // 25: client only_local flag + uint8_t type; // 26: client socket type (CFG_SERVER_TYPE_*) + // V2 fields (NAT_DIRECT detection): + uint8_t src_ipv4[4]; // 27: client interface_addr IPv4 (big-endian, 0 if N/A) + uint8_t src_port[2]; // 31: client interface_addr port (big-endian) + uint8_t collision; // 33: 1 = cross-connect, remote claims master + uint8_t ed25519_pubkey[SC_PUBKEY_SIZE]; // 34: client Ed25519 pubkey (32 bytes) + // V4 fields: + uint8_t device_type; // 66: CLIENT_TYPE_SERVER/DESKTOP/MOBILE +} __attribute__((packed)); +#define ETCP_INIT_REQ_SIZE sizeof(struct ETCP_INIT_REQUEST_PKT) + +// INIT RESPONSE: server → client +struct ETCP_INIT_RESPONSE_PKT { + uint8_t code; // 0: ETCP_INIT_RESPONSE (0x03) или ETCP_INIT_RESPONSE_NOINIT (0x05) + uint8_t node_id[8]; // 1: server node_id (big-endian) + uint8_t reset_id[8]; // 9: reset epoch id (big-endian) + uint8_t mtu[2]; // 17: server MTU (big-endian) + uint8_t link_id; // 19: server's local link id + uint8_t remote_socket_id; // 20: echo of client's socket_id + uint8_t only_local; // 21: server only_local flag + uint8_t type; // 22: server socket type (CFG_SERVER_TYPE_*) + // V2 fields: + uint8_t peer_ipv4[4]; // 23: client's external NAT address (big-endian) + uint8_t peer_port[2]; // 27: client's external NAT port (big-endian) + // V3 fields: + uint8_t ed25519_pubkey[SC_PUBKEY_SIZE]; // 29: server Ed25519 pubkey (32 bytes) + // V4 fields: + uint8_t device_type; // 61: CLIENT_TYPE_SERVER/DESKTOP/MOBILE + uint8_t keepalive[2]; // 62: server keepalive interval (big-endian) +} __attribute__((packed)); +#define ETCP_INIT_RESP_SIZE sizeof(struct ETCP_INIT_RESPONSE_PKT) + +typedef void (*etcp_ping_callback_t)(int success, uint16_t rtt, void* arg, uint64_t nonce, + const uint8_t* resp_data, size_t resp_data_len); +struct PING_CONTEXT { + struct PING_CONTEXT* next; + struct UTUN_INSTANCE* instance; + etcp_ping_callback_t cb; + void* arg; + uint64_t nonce; + void* timeout_timer; + uint64_t send_time; // время отправки пинга в 0.1ms + uint8_t peer_pubkey[SC_PUBKEY_SIZE]; + uint8_t socket_id; + uint64_t socket_generation; + struct sockaddr_storage peer_addr; + uint8_t* user_data; + size_t user_data_len; +}; + +#define LINK_ADDR_KEY_SIZE 19 // port(2) + addr(16) + family(1) + +struct link_queue_entry { + uint8_t key[LINK_ADDR_KEY_SIZE]; + struct ETCP_LINK* link; +}; + +// список активных подключений которые обслуживает сокет. каждый сокет может обслуживать много подключений +struct ETCP_SOCKET { + struct ETCP_SOCKET* next; // Linked list для всех соединений + struct UTUN_INSTANCE* instance; + char name[MAX_CONN_NAME_LEN]; + socket_t fd; // UDP socket / TCP listen socket + int mtu; + + uint8_t type; // CFG_SERVER_TYPE_* + uint8_t sock_id; // unique id (0-255) for NAT matching + uint8_t is_tcp; // 1=TCP (STCP), 0=UDP + uint8_t probe_only; // вспомогательный UDP-порт наблюдателя, не транспорт NCD + uint64_t generation; // случайная эпоха endpoint; sock_id может переиспользоваться + uint8_t reality_enabled; // 1=TCP-порт с REALITY-камуфляжем + uint8_t only_local; // 1 = only local, no forwarding (UDP) + uint32_t netif_index; // для sin6_scope_id + struct sockaddr_storage local_addr; + struct sockaddr_storage interface_addr; + struct sockaddr_storage nat_addr; + + /* ── uasync handle (UDP read_cb / TCP accept_cb) ── */ + void* socket_id; + + /* ── UDP-only ── */ + struct ll_queue* links_queue; // hash-очередь линков + int errorcode; + size_t pkt_format_errors; + struct address_profile address_profile; + uint32_t local_defaultroute_ip; // auto-detected IPv4 (network byte order) + uint8_t local_defaultroute_ip6[16]; + struct socks_udp* socks_udp; // SOCKS5 UDP ASSOCIATE (NULL = напрямую) +}; + +#define LINK_STATE_HANDSHAKE 1 +#define LINK_STATE_TRY_RECONNECT 2 +#define LINK_STATE_CONNECTED 3 + +// Тип функции-перехватчика отправки UDP. +// Если link->send_hook != NULL, etcp_udp_send вызывает её вместо socket_sendto. +struct ETCP_LINK; +typedef ssize_t (*etcp_udp_send_fn_t)(socket_t fd, const void* buf, size_t len, + const struct sockaddr* addr, socklen_t addr_len, + struct ETCP_LINK* link, void* context); + +struct socks_udp; + + +// ETCP Link - одно динамическое соединение (один путь) +struct ETCP_LINK { + struct ll_entry* link_queue_entry; // элемент в socket->links_queue + struct ETCP_LINK* next; // Linked list подключений для ETCP_CONN (каждое подключение это child для ETCP_CONN) + + struct ETCP_CONN* etcp; // подключение (parent) + struct ETCP_SOCKET* conn; // сокет через который работаем + // Путь соединения + struct sockaddr_storage remote_addr; // Удалённый адрес + + // Параметры соединения + uint16_t mtu; // MTU общий + uint16_t mtu_local; // MTU моего зла + uint16_t mtu_remote; // MTU удаленного узла + uint16_t keepalive_interval; // Keepalive интервал (x1ms) + uint32_t recovery_interval; // recovery интервал (x0.1ms) + uint8_t is_server; // инициирует подключение клиент + uint8_t initialized; // Флаг инициализации (1=подтверждено или получен request) + uint8_t local_link_id; // id моего линка + uint8_t remote_link_id; // id этого линка на peer (устанавливается в момент initialized) + uint8_t remote_socket_id; // socket id peer + uint8_t remote_only_local; // only_local flag from peer + uint8_t remote_type; // CFG_SERVER_TYPE_* (type of peer's socket) + uint8_t recv_keepalive; // 1 - up, 0 - down (принимаются ли пакеты) + uint8_t remote_keepalive; // 1 - up, 0 - down (удаленная сторона сообщает - принимаются ли у нее пакеты) + uint8_t link_status; // 1 - up, 0 - down (итоговый статус - если есть проблемы на любой стороне - линк down) + uint8_t link_state; // 0 - just init, 1 - handshake, 2 - try reconnect, 3 - connected + + // Состояние установки соединения (только для клиентов) + void* init_timer; // Таймер для повторов INIT (NULL=не подключается) + uint16_t init_timeout; // Текущий таймаут в мс + uint16_t init_retry_count; // Счетчик попыток + +// uint64_t last_activity; // Время последней активности + uint64_t last_recv_local_time; // x0.1 ms + uint64_t rx_deadline_tb; // конечный срок ожидания RX, включая объявленный сон + void* rx_timeout_timer; // проверка жизни независимо от отправки keepalive + uint16_t last_recv_timestamp; + uint8_t last_recv_updated; // =1 при обновлении timestamp, =0 при отправке (чтобы не дублировать отправки при отсутствии обновлений) + + uint64_t shaper_load_time_tb; // основной в 0.1 мс units + uint64_t shaper_sub_nanotime; // sub 0-999999 для 0.1 нс - 0.1 мс + uint8_t shaper_state; + void* shaper_timer; + + uint32_t inflight_bytes; + uint32_t inflight_packets; + uint32_t inflight_lim_bytes; + + // statistics + size_t encrypt_errors; + size_t decrypt_errors; + size_t send_errors; + size_t recv_errors; + size_t total_encrypted; + size_t total_decrypted; + uint32_t total_retransmissions; + uint64_t acked_bytes; // подтверждённых байт на этом линке + uint64_t acked_packets; // подтверждённых пакетов на этом линке + + uint16_t rtt_last; // round trip (время отправки + приёма) + uint32_t jitter; // Current jitter [>>16] x0.1 ms + // rtt_avg10 удалён — заменён BBR (bbr->min_rtt_us/100) + uint32_t recv_dt_avg_tx; // дельта времени для отправленных пакетов (относительное время отправки) x256 + uint32_t recv_dt_avg_rx; // дельта времени для принятых пакетов (относительное время отправки) x256 + + uint16_t tt_last; // transmit time (время доставки отправленных пакетов) + uint16_t rt_last; // recv time (время доставки принятых пакетов) + uint32_t bandwidth; // Link bandwidth in Kbits/sec + + // NAT address tracking (from INIT_RESPONSE) + struct sockaddr_storage local_as_seen_by_peer; // только наш endpoint, наблюдаемый удалённым пиром + uint32_t nat_changes_count; // Counter of NAT address changes + uint32_t nat_hits_count; // Counter of NAT address matches (new init response with same IP:port) + + // Keepalive state + void* keepalive_timer; // Таймер обычного keepalive (только в normal-режиме) + uint32_t keepalive_timeout; // таймаут (ms) + uint16_t ka_period_ms; // текущий период отправки keepalive + uint16_t ka_sleep_period_ms; // период keepalive в спячке (мс; настройка клиента / ka_period_ms пира) + uint8_t ka_my_sleeping; // 1 = я объявил сон (standby) + uint8_t ka_peer_sleeping; // 1 = пир объявил сон + uint8_t ka_sleep_acked; // получен ACK моего анонса сна + uint16_t ka_active_remaining; // сколько до SLEEP-фазы (x0.1с), 0 = уже спим + uint16_t ka_sleep_duration; // длительность SLEEP-фазы (x0.1с) + void* ka_sleep_announce_timer; // таймер ретрая анонса спячки (ACTIVE-окно) + uint8_t ka_announce_attempts; // попыток анонса в текущем окне + uint8_t ka_announce_max_attempts; // лимит попыток на окно (сколько влезет, но ≥ min) + uint8_t ka_fail_count; // кумулятивные провалы (сброс по ACK), ≥ KA_SLEEP_MAX_FAILS → down + uint8_t pkt_sent_since_keepalive; // Флаг: был ли отправлен пакет с последнего keepalive тика + uint32_t keepalive_sent_count; // Счётчик отправленных keepalive + uint32_t keepalive_recv_count; // Счётчик полученных keepalive + uint8_t peer_device_type; // CLIENT_TYPE_* удалённой стороны (из handshake) + uint16_t handshake_minsize; // минимальный размер udp при handshake + uint16_t handshake_maxsize; // мax размер udp при handshake (выбирает рандом) + + // === Burst-измерение bandwidth === + // Sender-сторона + uint16_t burst_id; // монотонно возрастающий ID burst + uint8_t burst_active; // 1 = burst в процессе отправки + uint8_t burst_seq; // текущий номер пакета в burst (0..burst_count-1) + uint8_t burst_count; // общее число пакетов в burst + uint64_t burst_last_time_tb; // время последнего burst (0.1ms), для MIN_BURST_INTERVAL + void* burst_resp_timer; // таймер ожидания ответа на burst + uint32_t burst_target_bdp; // BDP из последнего успешного burst (байты) + uint16_t burst_pkt_size; // сохранённый размер пакета для вычисления BW + + // Receiver-сторона + uint16_t burst_recv_id; // ID отслеживаемого burst + uint8_t burst_recv_count; // ожидаемое число пакетов в burst + uint8_t burst_recv_next_seq; // следующий ожидаемый seq + uint8_t burst_recv_valid; // 1 = порядок пакетов не нарушен + uint8_t burst_recv_received; // сколько пакетов уже принято + uint64_t burst_recv_times[16]; // время прихода каждого пакета (µs) + uint16_t burst_recv_pkt_size; // размер пакета из MEAS_TS (для ответа) + uint8_t burst_resp_pending; // 1 = есть готовый ответ для piggyback + uint32_t burst_resp_gap_avg; // средний inter-packet gap (µs) + uint32_t burst_resp_gap_min; // минимальный inter-packet gap (µs) + uint8_t burst_resp_pkt_count; // число пакетов в измерении + uint8_t burst_resp_valid; // валидность измерения + + // === BBR congestion control === + struct bbr* bbr; // BBR state (per-link, создаётся в etcp_link_new) + uint64_t delivered_bytes; // кумулятивно delivered bytes на линке (для round detection) + uint64_t last_ack_time_tb; // 0.1ms timestamp последнего ACK (для interval_us) + uint32_t bbr_pacing_rate; // bytes/sec output BBR → shaper + uint32_t bbr_loss_since_ack; // счётчик потерь на линке с последнего ACK (для rs->lost) + + etcp_udp_send_fn_t send_hook; // NULL = socket_sendto напрямую + void* send_hook_ctx; + + struct sockaddr_storage local_bound_addr; // локальный адрес к которому привязан линк (interface_addr на момент создания) + uint8_t is_tcp; // 1 = TCP транспорт (STCP) + uint8_t reality_set; // 1 = link->reality валиден (реклама пира из BGP) + struct reality_client_config reality; // reality-параметры пира (для исходящего TCP) + struct stcp_link *tcp_link; // STCP линк (для TCP) + void *tcp_reconnect_timer; // таймер реконнекта (TCP клиент) + uint32_t tcp_reconnect_delay_ms; // задержка реконнекта (экспоненциальный backoff) +}; + +// Единая точка отправки всех ETCP-пакетов. +ssize_t etcp_udp_send(struct ETCP_LINK* link, socket_t fd, const void* buf, size_t len, + const struct sockaddr* addr, socklen_t addr_len); + +// INITIALIZATION +// Создаёт только listen-сокеты из конфига (серверы для incoming connections) +int init_sockets(struct UTUN_INSTANCE* instance); +// Создаёт listen-сокеты и client connections из конфига +int init_connections(struct UTUN_INSTANCE* instance); + +// Заменяет принадлежащие конфигурации NCD handles. Новые приобретаются до освобождения старых. +int init_client_connections(struct UTUN_INSTANCE* instance, struct CFG_CLIENT* clients); + +// SOCKET FUNCTIONS +// добавляет новый версер (сокет для приёма и отправки кодограмм. обслуживает много подключений) +struct ETCP_SOCKET* etcp_socket_add(struct UTUN_INSTANCE* instance, struct CFG_SERVER* server); +// удаляет сокет и освобождает ресурсы (грохает все его подключения и сокет) +void etcp_socket_remove(struct ETCP_SOCKET* conn); + +// TCP socket functions (uses unified ETCP_SOCKET with is_tcp=1) +struct ETCP_SOCKET* tcp_socket_add(struct UTUN_INSTANCE* instance, struct CFG_SERVER* server); +void tcp_socket_remove(struct ETCP_SOCKET* sock); + +// Создаёт один серверный сокет (UDP или TCP) из CFG_SERVER. Линки не добавляет. +int etcp_create_server_socket(struct UTUN_INSTANCE* instance, struct CFG_SERVER* server, struct ETCP_SOCKET** out_sock); + +struct stcp_link; +struct stcp_client; +void tcp_server_on_link(struct stcp_link *link, struct ETCP_SOCKET *tcp_sock); + +// connection functions +// создает новый канал связи для etcp подключения (ETCP_CONN) +struct ETCP_LINK* etcp_link_new(struct ETCP_CONN* etcp, struct ETCP_SOCKET* conn, + const struct sockaddr_storage* remote_addr, uint8_t is_server); +void etcp_link_update_inflight_lim(struct ETCP_LINK* link, uint32_t new_lim); +void etcp_link_close(struct ETCP_LINK* link); +//int etcp_input_cbk(struct packet_buffer* pkt, struct ETCP_SOCKET* conn);// получает расшифрованный пакет +int etcp_encrypt_send(struct ETCP_DGRAM* dgram);// зашифровывает и отправляет пакет +// find link by address +struct ETCP_LINK* etcp_link_find_by_addr(struct ETCP_SOCKET* e_sock, const struct sockaddr_storage* addr, int is_tcp); + +// Выбор исходящего линка для collision INIT по принимающему сокету и адресу отправителя. +// Линк через другой локальный сокет не подходит. NULL — встречной попытки на этой паре нет. +struct ETCP_LINK* etcp_select_collision_link(struct ETCP_CONN* conn, struct ETCP_SOCKET* e_sock, + const struct sockaddr_storage* addr); + +// find free local_link_id for connection +// scans all links in connection, marks used ids in bit array +// returns first free id (0-255) or -1 if all occupied +int etcp_find_free_local_link_id(struct ETCP_CONN* etcp); + +// Burst measurement functions +void etcp_link_burst_start(struct ETCP_LINK* link); +void etcp_link_burst_check(struct ETCP_LINK* link); +void etcp_link_burst_finish(struct ETCP_LINK* link); + +void etcp_link_enter_init(struct ETCP_LINK *link); +void etcp_link_enter_reinit(struct ETCP_LINK *link); +void etcp_link_enter_ready_tcp(struct ETCP_LINK *link); +void etcp_tcp_link_start_connect(struct ETCP_LINK *link, struct sockaddr_storage *addr, uint16_t port); +void etcp_tcp_link_start_reconnect(struct ETCP_LINK *link); + +int etcp_packet_decrypted(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, + struct ETCP_LINK* link, size_t pkt_len); + +int etcp_send_ping(struct UTUN_INSTANCE* instance, const uint8_t* peer_pubkey_bin, + const struct sockaddr_storage* addr, int timeout_ms, + etcp_ping_callback_t cb, void* user_arg, + const uint8_t* user_data, size_t user_data_len); + +// шлёт пинг на addr используя pubkey. в пакет помещает user_data[user_len]. при получении ответа или таймауте вызывает callback. +// flags: ETCP_PING_FLAG_SEND_RTT — user_data содержит RTT для peer +int etcp_send_ping_to_socket(struct UTUN_INSTANCE* instance, struct ETCP_SOCKET* e_sock, + const uint8_t* peer_pubkey_bin, const struct sockaddr_storage* addr, + int timeout_ms, etcp_ping_callback_t cb, void* user_arg, + const uint8_t* user_data, size_t user_data_len, + uint8_t flags); + +/* Отмена без callback: вызывающий владеет arg и освобождает его после отмены. */ +void etcp_cancel_pings(struct UTUN_INSTANCE *instance, etcp_ping_callback_t callback, void *arg); + +// TCP-ping: подключение к TCP-адресу peer + хендшейк с флагом PING. RTT меряется +// по хендшейку (send→response). cb вызывается один раз (success=1/rtt или success=0). +// out_cli (опционально) — возвращает хендл stcp_client для досрочной отмены (stcp_client_destroy). +int etcp_send_tcp_ping(struct UTUN_INSTANCE* instance, const uint8_t* peer_pubkey_bin, + const struct sockaddr_storage* addr, int timeout_ms, + etcp_ping_callback_t cb, void* user_arg, + struct stcp_client** out_cli, const struct reality_client_config *reality); + +void etcp_connections_read_callback_socket(socket_t sock, void* arg); + + +#ifdef __cplusplus +} +#endif +#endif // ETCP_CONNECTIONS_H diff --git a/src/transport_layer/etcp_padding.h b/src/transport_layer/etcp_padding.h index ddc8a991..46332abb 100644 --- a/src/transport_layer/etcp_padding.h +++ b/src/transport_layer/etcp_padding.h @@ -1,4 +1,7 @@ -/* Независимый диапазон padding владельца; таймер и выбор длины работают в его uasync-потоке. */ +/* etcp_padding — выбор длины случайного дополнения сетевых пакетов для instance или ETCP-соединения. + * Обнулить ETCP_PADDING → start → size перед отправкой → stop до освобождения владельца. + * Start выбирает диапазон длин и включает таймер его обновления; size учитывает свободное место пакета. + * Все вызовы — в uasync-потоке владельца. Модуль выбирает длину, байты дополнения заполняет отправитель. */ #ifndef ETCP_PADDING_H #define ETCP_PADDING_H @@ -15,18 +18,28 @@ struct UTUN_INSTANCE; #define ETCP_PADDING_MAX 158 struct ETCP_PADDING { - uint16_t min, max; - void* timer; - struct UASYNC* ua; + uint16_t min, max; // включительные границы длины дополнения, байты + void* timer; // принадлежащий владельцу таймер обновления диапазона + struct UASYNC* ua; // заимствованный цикл событий до stop const char* name; // заимствованное имя владельца для диагностики - uint64_t next_refresh_tb; + uint64_t next_refresh_tb; // следующий срок обновления в единицах 0.1 мс; 0 после stop }; +// ── Жизненный цикл диапазона ── + +// Запустить обновление для обнулённого/остановленного состояния; name заимствован до stop. 0/-1. int etcp_padding_start(struct ETCP_PADDING* padding, struct UASYNC* ua, const char* name); + +// Отменить таймер и освободить ссылку на ua; NULL и повтор допустимы. void etcp_padding_stop(struct ETCP_PADDING* padding); -/* NULL допустим только для автономного STCP без instance: диапазон генерируется на операцию. */ + +// ── Выбор длины и диапазона владельца ── + +/* Выбрать длину в байтах, не превышающую available; -1 при неверном диапазоне или нехватке места для min. + * NULL допустим только для автономного STCP без instance: диапазон генерируется на операцию. */ int etcp_padding_size(const struct ETCP_PADDING* padding, size_t available); -/* Существующий незакрываемый ETCP_CONN владеет своим диапазоном; иначе используется instance. */ + +// Заимствованный диапазон существующего незакрываемого ETCP_CONN, иначе instance; NULL при неверных аргументах. const struct ETCP_PADDING* etcp_padding_for_peer(struct UTUN_INSTANCE* inst, const uint8_t* public_key); #ifdef __cplusplus diff --git a/src/transport_layer/etcp_padding.h1 b/src/transport_layer/etcp_padding.h1 new file mode 100644 index 00000000..ddc8a991 --- /dev/null +++ b/src/transport_layer/etcp_padding.h1 @@ -0,0 +1,36 @@ +/* Независимый диапазон padding владельца; таймер и выбор длины работают в его uasync-потоке. */ +#ifndef ETCP_PADDING_H +#define ETCP_PADDING_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct UASYNC; +struct UTUN_INSTANCE; + +#define ETCP_PADDING_MAX 158 + +struct ETCP_PADDING { + uint16_t min, max; + void* timer; + struct UASYNC* ua; + const char* name; // заимствованное имя владельца для диагностики + uint64_t next_refresh_tb; +}; + +int etcp_padding_start(struct ETCP_PADDING* padding, struct UASYNC* ua, const char* name); +void etcp_padding_stop(struct ETCP_PADDING* padding); +/* NULL допустим только для автономного STCP без instance: диапазон генерируется на операцию. */ +int etcp_padding_size(const struct ETCP_PADDING* padding, size_t available); +/* Существующий незакрываемый ETCP_CONN владеет своим диапазоном; иначе используется instance. */ +const struct ETCP_PADDING* etcp_padding_for_peer(struct UTUN_INSTANCE* inst, const uint8_t* public_key); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/transport_layer/node_conn_direct.h b/src/transport_layer/node_conn_direct.h index 89d4f194..82f9ab2b 100644 --- a/src/transport_layer/node_conn_direct.h +++ b/src/transport_layer/node_conn_direct.h @@ -1,30 +1,20 @@ /* - * node_conn_direct — общий прямой мультиадресный транспорт к узлу по node_id для BGP/topo_group. - * Адреса узла используются для создания и обновления линков общего ETCP-соединения. - * - * Архитектурная роль NODE_CONN_DIRECT (NCD handle): - * - выражает владение прямым транспортом, в том числе во время установления связи; - * - группа удерживает его для своей BGP/topo_group-сессии; - * - временный handle попытки подключения после установления связи должен перейти группе - * либо быть закрыт; передачу владения выполняют через node_conn_direct_transfer(); - * - не выражает длительность звонка или передачи файла: эту потребность хранит CONN_MGR_HANDLE. - * CM подбирает оптимальный P2P-путь для больших объёмов данных (медиа, файлы, звонки), - * а NCD управляет прямым транспортом и его адресами независимо от конкретной медиа-операции. - * - * Модуль управляет ETCP-соединениями через непрозрачные handle'ы (NODE_CONN_DIRECT). - * Каждый владелец открывает свой handle; несколько handle разделяют ETCP_CONN. - * close снимает только своё владение. Последний handle работающего транспорта начинает - * CLOSE/KEEP_ALIVE (5 s либо срок пробуждения пира); DOWN удаляется по таймауту ETCP. - * - * Кто использует: - * - conn_mgr (Connection Manager) — временные handle'ы попыток установления прямого транспорта - * - topo_group — владеет транспортом групповой сессии с начала CONNECTING - * - протокол join — временный транспорт до добавления участника - * - любой модуль, кому нужно надёжное ETCP-соединение с конкретным node_id - * + * node_conn_direct (NCD) — прямой ETCP-транспорт к узлу по его ключу и сетевым адресам. + * Порядок: open → дождаться UP → get_conn → close. Каждый владелец открывает свой handle. + * Несколько handles делят один ETCP_CONN; close снимает только своё владение. + * NCD создаёт и обновляет UDP/STCP-линки, но не проверяет членство и готовность topo-группы. + * Для групповой сессии используйте topo_group_peer_open, для звонка/файла и выбора пути — conn_mgr_open. + * Все операции — в потоке uasync. + */ + +/* Владение транспортом: + * Группа держит NCD с начала CONNECTING; отдельные handles могут принадлежать CM и протоколу join. + * Временный handle закрывают либо передают новому владельцу через transfer. + * Потребность сервиса в доставке на всё время звонка/файла выражает CONN_MGR_HANDLE (conn_mgr.h). + * Последний handle работающего транспорта начинает CLOSE/KEEP_ALIVE (5 с либо срок пробуждения пира); + * DOWN-транспорт удаляется по таймауту ETCP. * Переходы UP/DOWN/TIMEOUT/CLOSED доставляются синхронно. Закрытие любого handle * из callback безопасно. Начальное UP при open готового conn откладывается через uasync_call_soon. - * Все операции — в потоке uasync. NCD не проверяет членство и готовность группы. */ #ifndef NODE_CONN_DIRECT_H #define NODE_CONN_DIRECT_H @@ -36,18 +26,18 @@ struct ETCP_CONN; struct TOPO_NODE; struct ETCP_SOCKET; -/* Привязать CLOSE/KEEP_ALIVE при запуске ядра, в том числе без исходящих handles. */ +// Привязать CLOSE/KEEP_ALIVE при запуске ядра, в том числе без исходящих handles. int node_conn_direct_init(struct UTUN_INSTANCE* inst); -/* Число локальных владельцев конкретного транспорта; внешние ETCP refs сюда не входят. */ +// Число локальных владельцев конкретного транспорта; внешние ETCP refs сюда не входят. int node_conn_direct_handle_count(const struct ETCP_CONN* conn); -/* ─── Return codes ─── */ +// ─── Return codes ─── #define NCD_NEW 0 /* создано новое соединение */ #define NCD_REUSED 1 /* переиспользовано существующее */ #define NCD_ERR -1 /* ошибка */ -/* ─── Opaque handle ─── */ +// ─── Opaque handle ─── struct NODE_CONN_DIRECT; /* Рабочий ETCP, совместимая исходящая пара либо возможность reverse на наш опубликованный адрес. @@ -55,7 +45,7 @@ struct NODE_CONN_DIRECT; * Учитывает IPv6 и исходящий TCP fallback; возможность попытки не доказывает достижимость. */ int node_conn_direct_compatible(struct UTUN_INSTANCE* inst, uint64_t node_id); -/* ─── Callback events (единый callback на handle) ─── */ +// ─── Callback events (единый callback на handle) ─── enum ncd_event { NCD_EVENT_UP = 0, /* соединение поднялось (линки работают) */ NCD_EVENT_DOWN = 1, /* соединение упало */ @@ -88,7 +78,7 @@ int node_conn_direct_open_node(struct UTUN_INSTANCE* inst, uint64_t node_id, struct TOPO_NODE* ni, /* публичный ключ + адреса пира (не владеем) */ struct ETCP_SOCKET* specific_sock); /* NULL — авто-подбор */ -/* Освободить handle и отменить его callbacks; другие владельцы сохраняют транспорт. */ +// Освободить handle и отменить его callbacks; другие владельцы сохраняют транспорт. void node_conn_direct_close(struct NODE_CONN_DIRECT* h); /* Применить принятую запись из общего реестра к уже существующему соединению. @@ -108,13 +98,13 @@ void node_conn_direct_set_callback(struct NODE_CONN_DIRECT* h, ncd_callback cb, * должен забыть указатель. Соединение не трогается. */ void node_conn_direct_transfer(struct NODE_CONN_DIRECT* h, ncd_callback new_cb, void* new_cb_arg); -/* Идентификатор узла; 0 для NULL. */ +// Идентификатор узла; 0 для NULL. uint64_t node_conn_direct_node_id(const struct NODE_CONN_DIRECT* h); -/* Заимствованный транспорт, в том числе до UP; NULL после CLOSED. Не закрывать напрямую. */ +// Заимствованный транспорт, в том числе до UP; NULL после CLOSED. Не закрывать напрямую. struct ETCP_CONN* node_conn_direct_get_conn(struct NODE_CONN_DIRECT* h); -/* ─── Протокол CLOSE / KEEP_ALIVE (ETCP_ID_NCD_CONTROL = 0x12) ─── */ +// ─── Протокол CLOSE / KEEP_ALIVE (ETCP_ID_NCD_CONTROL = 0x12) ─── #define NCD_SUBCMD_CLOSE 0x01 #define NCD_SUBCMD_KEEP_ALIVE 0x02 @@ -129,7 +119,7 @@ struct ncd_control_msg { #define NCD_CONTROL_MSG_SIZE sizeof(struct ncd_control_msg) -/* ─── Управление линками на конкретном сокете ─── */ +// ─── Управление линками на конкретном сокете ─── /* * ncd_add_socket_links — итерирует все активные NCD-соединения (ncd_registry), * для каждого загружает TOPO_NODE с адресами пира и создаёт линки (etcp_link_new) diff --git a/src/transport_layer/node_conn_direct.h1 b/src/transport_layer/node_conn_direct.h1 new file mode 100644 index 00000000..89d4f194 --- /dev/null +++ b/src/transport_layer/node_conn_direct.h1 @@ -0,0 +1,149 @@ +/* + * node_conn_direct — общий прямой мультиадресный транспорт к узлу по node_id для BGP/topo_group. + * Адреса узла используются для создания и обновления линков общего ETCP-соединения. + * + * Архитектурная роль NODE_CONN_DIRECT (NCD handle): + * - выражает владение прямым транспортом, в том числе во время установления связи; + * - группа удерживает его для своей BGP/topo_group-сессии; + * - временный handle попытки подключения после установления связи должен перейти группе + * либо быть закрыт; передачу владения выполняют через node_conn_direct_transfer(); + * - не выражает длительность звонка или передачи файла: эту потребность хранит CONN_MGR_HANDLE. + * CM подбирает оптимальный P2P-путь для больших объёмов данных (медиа, файлы, звонки), + * а NCD управляет прямым транспортом и его адресами независимо от конкретной медиа-операции. + * + * Модуль управляет ETCP-соединениями через непрозрачные handle'ы (NODE_CONN_DIRECT). + * Каждый владелец открывает свой handle; несколько handle разделяют ETCP_CONN. + * close снимает только своё владение. Последний handle работающего транспорта начинает + * CLOSE/KEEP_ALIVE (5 s либо срок пробуждения пира); DOWN удаляется по таймауту ETCP. + * + * Кто использует: + * - conn_mgr (Connection Manager) — временные handle'ы попыток установления прямого транспорта + * - topo_group — владеет транспортом групповой сессии с начала CONNECTING + * - протокол join — временный транспорт до добавления участника + * - любой модуль, кому нужно надёжное ETCP-соединение с конкретным node_id + * + * Переходы UP/DOWN/TIMEOUT/CLOSED доставляются синхронно. Закрытие любого handle + * из callback безопасно. Начальное UP при open готового conn откладывается через uasync_call_soon. + * Все операции — в потоке uasync. NCD не проверяет членство и готовность группы. + */ +#ifndef NODE_CONN_DIRECT_H +#define NODE_CONN_DIRECT_H + +#include + +struct UTUN_INSTANCE; +struct ETCP_CONN; +struct TOPO_NODE; +struct ETCP_SOCKET; + +/* Привязать CLOSE/KEEP_ALIVE при запуске ядра, в том числе без исходящих handles. */ +int node_conn_direct_init(struct UTUN_INSTANCE* inst); + +/* Число локальных владельцев конкретного транспорта; внешние ETCP refs сюда не входят. */ +int node_conn_direct_handle_count(const struct ETCP_CONN* conn); + +/* ─── Return codes ─── */ +#define NCD_NEW 0 /* создано новое соединение */ +#define NCD_REUSED 1 /* переиспользовано существующее */ +#define NCD_ERR -1 /* ошибка */ + +/* ─── Opaque handle ─── */ +struct NODE_CONN_DIRECT; + +/* Рабочий ETCP, совместимая исходящая пара либо возможность reverse на наш опубликованный адрес. + * Без линков/handles/таймеров/проб. Исходящие пары — тот же обход, что open; reverse — sock_match/публикуемые адреса. + * Учитывает IPv6 и исходящий TCP fallback; возможность попытки не доказывает достижимость. */ +int node_conn_direct_compatible(struct UTUN_INSTANCE* inst, uint64_t node_id); + +/* ─── Callback events (единый callback на handle) ─── */ +enum ncd_event { + NCD_EVENT_UP = 0, /* соединение поднялось (линки работают) */ + NCD_EVENT_DOWN = 1, /* соединение упало */ + NCD_EVENT_CLOSED = 3, /* транспорт окончательно удалён; get_conn=NULL, владелец закрывает handle */ + NCD_EVENT_TIMEOUT = 2, /* срок первой попытки истёк; удерживаемый handle может получить поздний UP */ +}; + +typedef void (*ncd_callback)(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg); + +/* + * Открыть handle к узлу по node_id. + * Node info ищется через node_registry, fallback — SQLite. + * cb вызывается при изменении статуса / таймауте. + * Если conn уже готов — cb(NCD_EVENT_UP) через uasync_call_soon (не синхронно). + * До первой отправки дождаться UP. Результат: NCD_NEW/NCD_REUSED или NCD_ERR. + * При успехе вызывающий обязан закрыть *out_handle, в том числе после CLOSED. + */ +int node_conn_direct_open(struct UTUN_INSTANCE* inst, uint64_t node_id, + ncd_callback cb, void* cb_arg, + struct NODE_CONN_DIRECT** out_handle, + struct ETCP_SOCKET* specific_sock); /* NULL — авто-подбор всех совместимых сокетов для линков */ + +/* + * То же, но с уже загруженным TOPO_NODE (ni не владеем — можно передать временный на стеке). + * ni должен содержать public_key, v4_addrs/v6_addrs для создания линков. + */ +int node_conn_direct_open_node(struct UTUN_INSTANCE* inst, uint64_t node_id, + ncd_callback cb, void* cb_arg, + struct NODE_CONN_DIRECT** out_handle, + struct TOPO_NODE* ni, /* публичный ключ + адреса пира (не владеем) */ + struct ETCP_SOCKET* specific_sock); /* NULL — авто-подбор */ + +/* Освободить handle и отменить его callbacks; другие владельцы сохраняют транспорт. */ +void node_conn_direct_close(struct NODE_CONN_DIRECT* h); + +/* Применить принятую запись из общего реестра к уже существующему соединению. + * Не создаёт соединение и не приобретает handle. Число новых линков или -1 при ошибке. */ +int node_conn_direct_update_node(struct UTUN_INSTANCE* inst, uint64_t node_id); + +/* Как close, но последний handle закрывает транспорт сразу, без fin_wait. + * При наличии других handle общий транспорт и их callbacks сохраняются. */ +void node_conn_direct_force_close(struct NODE_CONN_DIRECT* h); + +/* Сменить или сбросить (cb=NULL) callback на уже открытом handle. + * Не влияет на refcounting и состояние conn. */ +void node_conn_direct_set_callback(struct NODE_CONN_DIRECT* h, ncd_callback cb, void* cb_arg); + +/* Передать владение handle новому владельцу (смена callback + arg). + * После вызова handle принадлежит new_cb_arg, прежний владелец + * должен забыть указатель. Соединение не трогается. */ +void node_conn_direct_transfer(struct NODE_CONN_DIRECT* h, ncd_callback new_cb, void* new_cb_arg); + +/* Идентификатор узла; 0 для NULL. */ +uint64_t node_conn_direct_node_id(const struct NODE_CONN_DIRECT* h); + +/* Заимствованный транспорт, в том числе до UP; NULL после CLOSED. Не закрывать напрямую. */ +struct ETCP_CONN* node_conn_direct_get_conn(struct NODE_CONN_DIRECT* h); + +/* ─── Протокол CLOSE / KEEP_ALIVE (ETCP_ID_NCD_CONTROL = 0x12) ─── */ + +#define NCD_SUBCMD_CLOSE 0x01 +#define NCD_SUBCMD_KEEP_ALIVE 0x02 + +#pragma pack(push, 1) +struct ncd_control_msg { + uint8_t cmd; /* ETCP_ID_NCD_CONTROL */ + uint8_t subcmd; /* NCD_SUBCMD_CLOSE / KEEP_ALIVE */ + uint64_t node_id; /* node_id отправителя */ +}; +#pragma pack(pop) + +#define NCD_CONTROL_MSG_SIZE sizeof(struct ncd_control_msg) + +/* ─── Управление линками на конкретном сокете ─── */ +/* + * ncd_add_socket_links — итерирует все активные NCD-соединения (ncd_registry), + * для каждого загружает TOPO_NODE с адресами пира и создаёт линки (etcp_link_new) + * на переданном сокете (только совместимые family: v4/v6). Аналог ncd_create_links + * но вызывается извне (auto_socket) при добавлении нового интерфейса. + * Возвращает количество созданных линков (0 если нечего добавить или ошибка). + */ +int ncd_add_socket_links(struct UTUN_INSTANCE* inst, struct ETCP_SOCKET* sock); + +/* + * ncd_remove_socket_links — итерирует все активные NCD-соединения, + * для каждого закрывает линки (etcp_link_close) привязанные к переданному сокету. + * Вызывается при удалении сокета (интерфейс пропал). Возвращает количество закрытых линков. + */ +int ncd_remove_socket_links(struct UTUN_INSTANCE* inst, struct ETCP_SOCKET* sock); + +#endif diff --git a/src/transport_layer/reality_io.h b/src/transport_layer/reality_io.h index f6c80cda..7ae54e09 100644 --- a/src/transport_layer/reality_io.h +++ b/src/transport_layer/reality_io.h @@ -1,4 +1,9 @@ -/* Поток записей отделён от внутреннего буфера STCP; все вызовы — в uasync-потоке владельца. +/* reality_io — защищённая внешняя оболочка STCP с TLS-подобным handshake и записями. + * STCP запускает client_start/server_start; ready разрешает внутренний STCP-handshake и последующий обмен через read/send. + * Cleanup освобождает состояние соединения; owner_cleanup — общие ресурсы instance. Все вызовы — uasync-поток владельца. + * Буфер оболочки отделён от внутреннего буфера STCP; TLS-совместимость этим модулем не обеспечивается. */ + +/* Протокол внешней оболочки: * CH -> SH + CCS + encrypted(EE/cover Certificate/cover CertificateVerify/Server Finished) * -> CCS + encrypted(Client Finished) -> encrypted(STCP handshake и последующий поток). * Проверка стороны использует Finished, cover Certificate/CertificateVerify не являются PKI. @@ -6,7 +11,7 @@ * "uTun-reality-record-v2"); в transcript входят handshake-заголовки, но не record-заголовки. * От root раздельно выводятся client-traffic/server-traffic и их key/iv/finished-ключи. * После собственного SH fallback запрещён. До него relay получает весь исходный буфер без нормализации. - * Таймеры/списки/кеш replay принадлежат instance; destroy принудительно закрывает I/O без ожидания callbacks. */ + * Таймеры/списки/кеш replay принадлежат instance; owner_cleanup принудительно закрывает I/O без ожидания callbacks. */ #ifndef REALITY_IO_H #define REALITY_IO_H #include "reality.h" @@ -23,17 +28,34 @@ struct reality_owner { struct reality_relay *relays; unsigned relay_count; }; + +// ── Запуск защищённого соединения ── + int reality_io_client_start(struct stcp_conn *c, const struct reality_client_config *cfg, uint32_t now, const struct ETCP_PADDING *padding, void (*ready)(struct stcp_conn *)); + int reality_io_server_start(struct stcp_conn *c, void (*ready)(struct stcp_conn *)); + +// ── Состояние и обмен данными ── + size_t reality_io_buffered(const struct stcp_conn *c); + const char* reality_io_state_name(const struct stcp_conn *c); + int reality_io_read(struct stcp_conn *c); + void reality_io_resume(struct stcp_conn *c); + int reality_io_send(struct stcp_conn *c, uint8_t *data, size_t len); + int reality_io_close_notify(struct stcp_conn *c); + +// ── Освобождение соединения и общих ресурсов ── + void reality_io_cleanup(struct stcp_conn *c); + void reality_owner_cleanup(struct reality_owner *owner); + #ifdef __cplusplus } #endif diff --git a/src/transport_layer/reality_io.h1 b/src/transport_layer/reality_io.h1 new file mode 100644 index 00000000..f6c80cda --- /dev/null +++ b/src/transport_layer/reality_io.h1 @@ -0,0 +1,40 @@ +/* Поток записей отделён от внутреннего буфера STCP; все вызовы — в uasync-потоке владельца. + * CH -> SH + CCS + encrypted(EE/cover Certificate/cover CertificateVerify/Server Finished) + * -> CCS + encrypted(Client Finished) -> encrypted(STCP handshake и последующий поток). + * Проверка стороны использует Finished, cover Certificate/CertificateVerify не являются PKI. + * Root = HKDF-SHA256(AuthKey || X25519(client_ephemeral, server_ephemeral), SHA256(CH || SH), + * "uTun-reality-record-v2"); в transcript входят handshake-заголовки, но не record-заголовки. + * От root раздельно выводятся client-traffic/server-traffic и их key/iv/finished-ключи. + * После собственного SH fallback запрещён. До него relay получает весь исходный буфер без нормализации. + * Таймеры/списки/кеш replay принадлежат instance; destroy принудительно закрывает I/O без ожидания callbacks. */ +#ifndef REALITY_IO_H +#define REALITY_IO_H +#include "reality.h" +#include "etcp_padding.h" +#ifdef __cplusplus +extern "C" { +#endif +struct stcp_conn; +struct reality_io; +struct reality_replay_cache; +struct reality_relay; +struct reality_owner { + struct reality_replay_cache *replay; + struct reality_relay *relays; + unsigned relay_count; +}; +int reality_io_client_start(struct stcp_conn *c, const struct reality_client_config *cfg, uint32_t now, + const struct ETCP_PADDING *padding, void (*ready)(struct stcp_conn *)); +int reality_io_server_start(struct stcp_conn *c, void (*ready)(struct stcp_conn *)); +size_t reality_io_buffered(const struct stcp_conn *c); +const char* reality_io_state_name(const struct stcp_conn *c); +int reality_io_read(struct stcp_conn *c); +void reality_io_resume(struct stcp_conn *c); +int reality_io_send(struct stcp_conn *c, uint8_t *data, size_t len); +int reality_io_close_notify(struct stcp_conn *c); +void reality_io_cleanup(struct stcp_conn *c); +void reality_owner_cleanup(struct reality_owner *owner); +#ifdef __cplusplus +} +#endif +#endif