diff --git a/AGENTS.md b/AGENTS.md index d5a2414c..fbba7ca4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -297,6 +297,20 @@ KEEPALIVE=21, ETCPROUTE=22, BBR=23, ETCP_DUMP=24 - `eim_nat.c/h` - Endpoint-Independent Mapping NAT engine - `nat_transport.c/h` - NAT transport layer (packet relay) +**Chat (src/chat/)** — децентрализованный P2P чат: сообщения и участники автоматически синхронизируются между всеми узлами канала без центрального сервера. +- `chat_core.c/h` - Главный модуль чата. Единственная точка входа из GUI: принять сообщение, создать канал, изменить настройку. Управляет БД и жизненным циклом всех chat-модулей +- `chat_core_priv.h` - Внутренний API для частей chat_core: общий контекст и утилиты БД. Снаружи не используется +- `chat_sync.c/h` - Связывает чат с ETCP-сетью: отслеживает появление/разрыв соединений, обновляет онлайн-статус узлов, запускает синхронизацию участников каналов, обрабатывает приглашения +- `chat_event.c/h` - Доставка событий из ядра чата в GUI. Единый механизм: любой модуль отправляет событие (новое сообщение, смена участников, статус), GUI получает через один обработчик +- `chat_setting.c/h` - Настройки чата: проверяет корректность значений, хранит текущие. Используется и headless-парсером конфига, и GUI. Не зависит от других модулей +- `db_sync.c/h` - Автоматическая репликация данных между подключёнными узлами. Запись на одном узле — появляется у всех остальных. Криптоподписи и цепочка хешей защищают от подделок +- `member_sync.c/h` - Списки участников каналов в актуальном состоянии у всех. Добавил/удалил участника — изменение разойдётся по всем узлам. Работает поверх merkle_sync +- `merkle_sync.c/h` - Быстрое сравнение и синхронизация данных между двумя узлами. Обмениваются контрольными суммами и передают только различия. Универсальный движок: не привязан к конкретному типу данных +- `chat_profile.c` - Собственный профиль: имя узла, сетевые адреса, сохранение UI-состояния (часть chat_core) +- `chat_channel.c` - Создание и настройка каналов: готовит таблицы в БД, генерирует криптоключи, запускает подключение ко всем участникам (часть chat_core) +- `chat_msg.c` - Отправка и приём сообщений: запись в локальную БД, автоматическая рассылка всем участникам канала (часть chat_core) +- `chat_status.c` - Сбор диагностики: текущее время, активные соединения, типы NAT. Отправляется в GUI (часть chat_core) + **Config (src/)** - `config_parser.c/h` - INI-style config file parsing - `config_updater.c/h` - Config file modification utilities @@ -304,9 +318,6 @@ KEEPALIVE=21, ETCPROUTE=22, BBR=23, ETCP_DUMP=24 **Control (src/)** - `control_server.c/h` - Control/monitoring server (etcpmon backend API) -**Транспорт сообщений (src/)** -- `db_sync.c/h` - Синхронизация БД между узлами - **Прокси (src/proxy/)** - `socks_proxy.c/h` - SOCKS5 прокси - `udp_proxy.c/h` - UDP прокси @@ -626,6 +637,7 @@ C-ядро: `libutun_lite` (выборочная компиляция нужны Сборка: CMake (headless, Linux) + Gradle/NDK (Android APK). **Подробная инструкция:** `tools/chatgui-android/AGENTS.md` +**Chat-модули:** все файлы из `src/chat/` (описаны выше в секции «Chat») компилируются в `libutun_lite`. ## Runtime - Запуск utun от root (для tun): `/home/vnc1/proj/utun3/utun_start.sh` diff --git a/src/chat/chat_sync.h b/src/chat/chat_sync.h index 62fc46ba..fa5550b0 100644 --- a/src/chat/chat_sync.h +++ b/src/chat/chat_sync.h @@ -1,3 +1,27 @@ +/* + * chat_sync.h — оркестратор синхронизации чата по ETCP + * + * При поднятии / разрыве ETCP-соединений: + * - обновляет online-статус пиров в БД и GUI + * - запускает member_sync (синхронизацию участников каналов) через merkle_sync + * - управляет кешем каналов (периодический refresh из БД, 30s) + * + * Обслуживает протокол входа в канал между joiner и inviter: + * CHANNEL_INFO_REQ → CHANNEL_INFO_RESP → CHANNEL_JOIN → WELCOME + * Все сообщения с Ed25519-верификацией. + * + * Обрабатывает invite-ссылки (chat_sync_connect_from_invite): + * - сохраняет pubkey и адреса invite-узла в БД (nodes, node_addresses) + * - создаёт TOPO_GROUP, запускает conn_mgr для подключения + * - после поднятия ETCP-соединения запускает join-протокол + * + * Использование: + * 1. chat_sync_init(inst) — вызывается при старте, биндит ETCP_RT_ID_CHAT_SYNC (0x30) + * 2. chat_sync_connect_from_invite(ch_id, node_id, pubkey, addrs, count) — вход по invite + * 3. chat_sync_join_channel(inst, ch_id, target_node_id) — вход через уже подключённый узел + * 4. chat_sync_destroy(inst) — вызывается при завершении + */ + #ifndef CHAT_SYNC_H #define CHAT_SYNC_H diff --git a/src/transport_layer/etcp.c b/src/transport_layer/etcp.c index e14621c8..45825673 100644 --- a/src/transport_layer/etcp.c +++ b/src/transport_layer/etcp.c @@ -993,10 +993,10 @@ static void etcp_link_ready_callback(struct ETCP_CONN* etcp) { } if (etcp->links_up==0) { - DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "switch ETCP down -> up"); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "[%s] etcp_link_ready_callback: links_up 0→1, calling etcp_on_up (initialized=%d tx_state=%d)", etcp->log_name, etcp->initialized, etcp->tx_state); etcp->links_up=1; etcp_on_up(etcp); - } else DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "skip on_up - ETCP already up"); + } else DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[%s] etcp_link_ready_callback: links_up=%d already up", etcp->log_name, etcp->links_up); if (etcp->tx_state!=ETCP_TX_STATE_LINK_WAIT) return; diff --git a/src/transport_layer/etcp_connections.c b/src/transport_layer/etcp_connections.c index 1c7705fd..cbac21a5 100644 --- a/src/transport_layer/etcp_connections.c +++ b/src/transport_layer/etcp_connections.c @@ -195,10 +195,10 @@ static void etcp_link_init_timer_cbk(void* arg) { } link->init_timer = uasync_set_timeout(link->etcp->instance->ua, link->init_timeout, link, etcp_link_init_timer_cbk, "link_init"); - if (link->link_state == 3 && link->initialized) return; - if (link->etcp->links_up > 0) return; /* another link is already UP on this connection */ - if (link->link_state == 1) etcp_link_send_init(link,1,0);// init (with etcp reset) - else etcp_link_send_init(link,0,0);// no etcp reset (reinit) + if (link->link_state == 3 && link->initialized) { DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[%s] init_timer: SUPPRESSED state=%d init=%d link_status=%d recv_ka=%d remote_ka=%d links_up=%d", link->etcp->log_name, link->link_state, link->initialized, link->link_status, link->recv_keepalive, link->remote_keepalive, link->etcp->links_up); return; } + if (link->etcp->links_up > 0) { DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[%s] init_timer: SUPPRESSED (links_up=%d > 0)", link->etcp->log_name, link->etcp->links_up); return; } + if (link->etcp->got_initial_pkt == 0) etcp_link_send_init(link,1,0); + else etcp_link_send_init(link,0,0); } void etcp_link_restart_init_timer(struct ETCP_LINK* link) { @@ -223,7 +223,8 @@ void etcp_link_enter_reinit(struct ETCP_LINK* link) { link->link_state = 2; // reconnect etcp_on_link_down(link->etcp); if (link->is_server != 0) return; - etcp_link_send_init(link,0,0);// init without reset + etcp_conn_reinit(link->etcp); + etcp_link_send_init(link,1,0); if (link->keepalive_timer) {// keepalive заменяяется reinit запросами uasync_cancel_timeout(link->etcp->instance->ua, link->keepalive_timer); @@ -323,7 +324,7 @@ static void keepalive_timer_cb(void* arg) { link->recv_keepalive = 0; link->link_status = 0; etcp_on_link_down(link->etcp); - DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[%s] Conn:%s Link down: link_id=%d ka=%d remote_ka=%d tmo: %d>%d", link->etcp->log_name, link->conn?link->conn->name:"???", link->local_link_id, link->recv_keepalive, link->remote_keepalive, elapsed, timeout_units); + DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[%s] Conn:%s Link down: link_id=%d state=%d init=%d ka=%d remote_ka=%d tmo: %llu>%llu els=%llums", link->etcp->log_name, link->conn?link->conn->name:"???", link->local_link_id, link->link_state, link->initialized, link->recv_keepalive, link->remote_keepalive, (unsigned long long)timeout_units, (unsigned long long)elapsed, (unsigned long long)(elapsed/10)); DEBUG_WARN(DEBUG_CATEGORY_KEEPALIVE, "[%s] Link %p (local_id=%d) recv status changed to DOWN - no packets for %llu ms", link->etcp->log_name, link, link->local_link_id, (unsigned long long)(elapsed/10)); } } @@ -1535,7 +1536,11 @@ static int handle_init_response_client(struct ETCP_SOCKET* e_sock, struct ETCP_D link->init_timer = NULL; } - + if (pkt_code == ETCP_INIT_RESPONSE && link->etcp->got_initial_pkt) { + DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] REINIT from client: INIT_RESPONSE(0x03) received, reinit conn=%p", + link->etcp->log_name, (void*)link->etcp); + etcp_conn_reinit(link->etcp); + } if (link->etcp->initialized == 0) { etcp_conn_ready(link->etcp); @@ -1908,13 +1913,13 @@ void etcp_connections_read_callback_socket(socket_t sock, void* arg) { if (ml->init_timer) { uasync_cancel_timeout(conn->instance->ua, ml->init_timer); ml->init_timer = NULL; } } - /* Sync session_id; reset only if new session (not when yielding to master) */ + /* Sync session_id; reset if remote is clean and we're dirty, or request reset if we're clean and remote is dirty */ conn->session_id = session_id; - if (!yielding && conn->got_initial_pkt) { - send_reset = 1; - DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "[%s] REINIT existing link: code=0x%02x sess=%08x→%08x got_init=%d initialized=%d links_up=%d", - conn->log_name, code, conn->session_id, session_id, conn->got_initial_pkt, conn->initialized, conn->links_up); - etcp_conn_reinit(conn); + if (req->code == ETCP_INIT_REQUEST) { + if (conn->got_initial_pkt) etcp_conn_reinit(conn); + send_reset = 0; + } else { + if (conn->got_initial_pkt == 0) send_reset = 1; } } @@ -1994,14 +1999,16 @@ process_decrypted: /* restore recv_keepalive BEFORE computing link_status = remote && local */ if (link->recv_keepalive != 1) { link->recv_keepalive = 1; - DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] Link %d status changed to UP - packet received", - link->etcp->log_name, link->local_link_id); + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[%s] Link %d recv_keepalive restored to 1 (was 0) link_status=%d remote_ka=%d state=%d init=%d links_up=%d", + link->etcp->log_name, link->local_link_id, link->link_status, link->remote_keepalive, link->link_state, link->initialized, link->etcp->links_up); } int was_up = link->link_status; link->link_status = link->remote_keepalive && link->recv_keepalive; - if (link->link_status && !was_up && link->initialized) + if (link->link_status && !was_up && link->initialized) { + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "[%s] Link %d status popped UP: recv_ka=%d remote_ka=%d state=%d init=%d → calling loadbalancer_link_ready", link->etcp->log_name, link->local_link_id, link->recv_keepalive, link->remote_keepalive, link->link_state, link->initialized); loadbalancer_link_ready(link); + } link->last_recv_local_time=get_time_tb(); link->last_recv_timestamp=pkt->timestamp; diff --git a/src/transport_layer/etcp_loadbalancer.c b/src/transport_layer/etcp_loadbalancer.c index 7ea8e39b..8cdb0fe0 100644 --- a/src/transport_layer/etcp_loadbalancer.c +++ b/src/transport_layer/etcp_loadbalancer.c @@ -194,7 +194,7 @@ void loadbalancer_link_ready(struct ETCP_LINK* link) { DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "link still blocked by shaper"); return; } - DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "link=%p now ready, notifying ETCP_CONN", link); + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[%s] loadbalancer_link_ready: link=%p links_up=%d initialized=%d tx_state=%d", link->etcp->log_name, (void*)link, link->etcp->links_up, link->etcp->initialized, link->etcp->tx_state); if (link->etcp->link_ready_for_send_fn) { link->etcp->link_ready_for_send_fn(link->etcp); } else { diff --git a/tools/chatgui-android/AGENTS.md b/tools/chatgui-android/AGENTS.md index 1c0fd7c6..9c32c068 100644 --- a/tools/chatgui-android/AGENTS.md +++ b/tools/chatgui-android/AGENTS.md @@ -13,8 +13,11 @@ tools/chatgui-android/ ├── libutun_lite/ # C-ядро (статическая библиотека) │ ├── CMakeLists.txt │ ├── utun_sources.cmake # список компилируемых .c файлов +│ ├── instance_lite.h/c # жизненный цикл (старт/стоп/рестарт, ключи) │ ├── utun_config_api.h/c # конфиг-провайдер (Kotlin → C) -│ └── invite_link_c.h/c # invite-ссылки (encode/decode, совместимы с десктопом) +│ ├── invite_link_c.h/c # invite-ссылки (encode/decode, совместимы с десктопом) +│ ├── voice_recorder.h/c # запись голосовых (PCM→Opus→канал) +│ └── attachment_sender.h/c # отправка файлов в канал │ ├── headless/ # CLI для Linux (тестирование без Android) │ ├── CMakeLists.txt @@ -61,6 +64,11 @@ tools/chatgui-android/ └── IMPL_PLAN.md # план реализации по этапам ``` +## Chat-подсистема (src/chat/) + +Все модули из `src/chat/` компилируются в `libutun_lite` через `file(GLOB_RECURSE)`. +Описания каждого модуля — см. корневой `AGENTS.md`, секция «Chat (src/chat/)». + ## Технологии | Слой | Язык | Фреймворк | @@ -233,22 +241,30 @@ log_udp_port = 9999 ## Ключевые файлы для доработок ### C-слой -- `libutun_lite/invite_link_c.h/c` — invite-ссылки (encode/decode, бинарный формат идентичен десктопу `tools/chatgui/src/invite_link.cpp`) -- `jni_bridge/android_jni_bridge.h/c` — bridge API: `utun_bridge_join_channel()`, `utun_bridge_send_message()`, JNI-функции -- `libutun_lite/utun_config_api.h/c` — конфиг-провайдер (Kotlin → C через коллбэки) -- `libutun_lite/utun_sources.cmake` — список всех .c файлов, компилируемых в libutun_lite +- `libutun_lite/instance_lite.h/c` — Жизненный цикл uTun для Android: запуск/остановка C-ядра в отдельном потоке, перезапуск при смене конфига, генерация и обновление X25519-ключей, health-check +- `jni_bridge/android_jni_bridge.h/c` — JNI-прослойка Kotlin↔C: все операции из UI (отправка сообщений, вход в каналы, голосовые, статус, настройки) и обратные вызовы (логи, события). Здесь же — JNI-функции, компилируемые только для Android +- `libutun_lite/invite_link_c.h/c` — Кодирование и декодирование invite-ссылок `utun://` в бинарный формат. Совместим с десктопной версией +- `libutun_lite/utun_config_api.h/c` — Поставщик конфигурации из Kotlin в C-ядро через callback-интерфейс (get_string, get_int64, get_int) +- `libutun_lite/voice_recorder.h/c` — Запись голосовых сообщений: накопление PCM-сэмплов с компрессором, кодирование в Opus-файл, отправка в канал через chat_core +- `libutun_lite/attachment_sender.h/c` — Отправка файлов в канал: копирование в media-директорию и регистрация через chat_core (media_index → db_sync) +- `libutun_lite/utun_sources.cmake` — Список всех .c файлов, компилируемых в libutun_lite. Новые файлы добавлять сюда ### Kotlin-слой -- `data/InviteLink.kt` — парсинг `utun://base64blob`, `deriveNodeId()` (SHA256 от pubkey), `serializeAddrs()` -- `data/NativeLib.kt` — JNI-обёртка, все `external fun native*` -- `ui/screens/JoinChannelDialog.kt` — диалог: ввод/вставка ссылки, debounce-декодинг, preview, Connect -- `ui/screens/QrScanScreen.kt` — CameraX + ML Kit сканер, фильтрует `utun://` ссылки -- `ui/screens/ChannelListScreen.kt` — список каналов, кнопка Join (QrCode), кнопка Create -- `MainActivity.kt` — навигация экранов, QrScan → JoinDialog flow - -### Headless -- `headless/headless_control.c` — управляющий TCP-сокет, команды: `join`, `connect`, `subscribe`, `debug_level` -- `headless/headless_main.c` — точка входа, парсинг аргументов, uasync event loop +- `data/NativeLib.kt` — Kotlin-обёртка над C-библиотекой: все вызовы из Kotlin транслируются в JNI-функции +- `data/ChatRepository.kt` — Хранилище данных: буферизация сообщений и каналов между C-ядром и UI через StateFlow +- `data/InviteLink.kt` — Разбор invite-ссылок: извлекает ID канала, публичный ключ, адреса для подключения +- `data/ConfigProvider.kt` — Поставщик конфигурации из Android DataStore в C-ядро через callback-интерфейс +- `data/LogManager.kt` — Сбор и хранение логов из C-ядра через log-callback +- `viewmodel/ChatViewModel.kt` — ViewModel: StateFlow-состояние для UI (список каналов, сообщения, статус) +- `ui/screens/ChannelListScreen.kt` — Главный экран: список каналов, кнопки Join (по invite-ссылке) и Create +- `ui/screens/ChatScreen.kt` — Экран чата: список сообщений + поле ввода +- `ui/screens/JoinChannelDialog.kt` — Диалог подключения к каналу: ввод/вставка invite-ссылки, предпросмотр, кнопка Connect +- `ui/screens/QrScanScreen.kt` — QR-сканер для invite-ссылок (CameraX + ML Kit) +- `MainActivity.kt` — Точка входа Android-приложения: навигация между экранами, QrScan → JoinDialog flow + +### Headless (тестирование без телефона) +- `headless/headless_main.c` — Точка входа headless-режима: парсинг аргументов, запуск C-ядра в uasync event loop +- `headless/headless_control.c/h` — Управляющий TCP-сокет: JSON-команды (`status`, `send`, `join`, `subscribe`) и асинхронные события ## Invite-ссылки (механика подключения к каналу)