135 Commits
Author SHA1 Message Date
evgeny d8cbe2e589 Add proxy bug list and lightsout game (desktop + Android) 2026-09-29 12:19:23 +03:00
evgeny e1946b208d Preserve proxy TCP streams across connect, backpressure and FIN 2026-09-29 04:00:34 +03:00
evgeny 09bc8c35c5 Release TUN proxy connections when lwIP aborts their PCB 2026-09-29 03:59:30 +03:00
evgeny f19ff11b36 Scope TCP proxy streams and restart messages to their peer 2026-09-29 03:58:53 +03:00
evgeny 268243feeb Isolate datagram proxy state and replies between instances and peers 2026-09-29 03:58:14 +03:00
evgeny 4be1e62251 Parse IPv4 options and reject malformed TUN proxy packets 2026-09-29 03:57:37 +03:00
evgeny 10b66e2ad0 Validate SOCKS5 authentication and destination addresses 2026-09-29 03:57:21 +03:00
evgeny 7a41fcf512 Notify TCP writers when the final synchronous send drains 2026-09-29 03:57:15 +03:00
evgeny 518df33b0e Fix UDP and ICMP packet construction at the TUN boundary 2026-09-29 03:56:55 +03:00
evgeny fdec5b65c5 Add independent radio compressor and Android audio controls 2026-09-29 03:37:47 +03:00
evgeny ce1f23fe46 Radio: stereo capture/playback with mono fallback; CAMCORDER source; bump RADIO_MAX_OPUS
- stereo capture: Android AudioRecord stereo (CAMCORDER source for stereo,
  voice sources fall back to CAMCORDER) + desktop separate miniaudio
  capture/playback devices (no asymmetric duplex)
- Opus stereo encode/decode, voice_jitter stereo (per-channel frame_size,
  slot-based SoundTouch)
- radio_audio_start takes explicit capture_channels (stereo->mono fallback)
- RADIO_MAX_OPUS 256->512 (128kbps stereo frames up to 320B)
- radio_capture_stereo setting + desktop radio settings UI (opus mode/bitrate,
  capture stereo)
- radio_send_to send stats logging (etcp_send ok/fail per second)
2026-09-29 02:31:41 +03:00
evgeny 331970a121 Radio: Opus mode/bitrate + mic source + PTT release tail; drop online status from member JSON 2026-09-29 00:40:10 +03:00
evgeny e3caf15f32 Guard routing loops and reconcile blocked transit against current paths
Track visited nodes outside the signed body for all routed packet types. Recheck queued transit routes without losing FIFO priority, reject unknown-group transit, and suppress BGP announcements caused only by RTT jitter.

Document routing rules and wire format. Validate graph churn, REINIT, encrypted transit, malformed paths, loop drops and queue cleanup; full suite: 106 passed, 1 skipped, plus proxy integration and sanitizer checks.
2026-09-28 15:36:47 +03:00
evgeny fa9e102271 Preserve config string terminator when preparing key updates 2026-09-28 15:16:57 +03:00
evgeny 39ce249361 Prune unconfirmed BGP paths and restart stalled exchanges 2026-09-28 14:58:13 +03:00
evgeny b76e6553bc Reconcile BGP paths and advertise selected routes per neighbor 2026-09-28 14:37:44 +03:00
evgeny 546e0c185d Fix WITHDRAW removing nodes with surviving paths 2026-09-28 14:20:55 +03:00
evgeny 4503bdfa7e diagnostics: explain and rate-limit rejected ETCP session data 2026-09-28 13:16:39 +03:00
evgeny c63e93715f fix: accept invite public keys with a leading zero byte 2026-09-28 12:26:13 +03:00
evgeny 904a9fbda1 docs: refresh development guides for current architecture 2026-09-28 12:10:39 +03:00
evgeny 01fcefdec5 docs: clarify core, group and chat header contracts 2026-09-28 12:01:08 +03:00
evgeny 72fee9e245 Separate core, UTUN and chat lifecycles with owned asynchronous work 2026-09-28 11:39:08 +03:00
evgeny 5838e63cde Use shared group requests for config and chat connection planners 2026-09-28 11:07:25 +03:00
evgeny 0756a6e020 Serialize group topology lazily through a cancellable backpressure sender 2026-09-28 02:45:00 +03:00
evgeny 792776a1f6 Negotiate group sessions and scope topology messages to peer epochs 2026-09-28 02:15:54 +03:00
evgeny 52ef7a4b3c Own recovery attempts in group sessions and require restored routes 2026-09-27 23:06:48 +03:00
evgeny 66c59501fc Track topology exchange readiness per group and peer 2026-09-27 22:14:03 +03:00
evgeny bce17d9cfc Route UTUN configuration and group ownership through NCD 2026-09-27 21:59:32 +03:00
evgeny 9745a987ac Define join contract and require committed CHAT membership for BGP 2026-09-27 20:34:24 +03:00
evgeny 1a8072e9b2 Release group NCD ownership when destroying peers 2026-09-27 20:08:17 +03:00
evgeny 26e6c05313 Apply fresh BGP addresses to shared NCD connections 2026-09-27 19:18:40 +03:00
evgeny ec51b2d61f Version signed node snapshots with persistent timestamps 2026-09-27 19:11:11 +03:00
evgeny 7374d1648c Support 8 KiB member owner tags without signature or serialization truncation 2026-09-27 17:12:13 +03:00
evgeny de714f5023 Add bounded Merkle pull lifecycle and terminal error handoff 2026-09-27 16:17:04 +03:00
evgeny 3e25bcef02 Refactor Merkle sync into bounded bidirectional rounds with atomic member updates 2026-09-27 14:44:39 +03:00
evgeny 1b92b4885b Restrict dynamic auto socket test to Linux 2026-09-27 12:48:40 +03:00
evgeny 10d3ecfda4 Record final root transport and full suite validation 2026-09-27 12:19:52 +03:00
evgeny 03f71fd94e Bound ETCP receive window and fix timestamp and allocator UB 2026-09-27 12:16:15 +03:00
evgeny 59f4802904 Fix autosocket ownership and exercise interface churn under load 2026-09-27 11:57:29 +03:00
evgeny 3131dcbb7c Fix ETCP session recovery and router lifecycle; confirm chat invites before publishing 2026-09-27 02:51:47 +03:00
evgeny 018c8e795d android: фикс UAF AudioTrack при переключении группы рации (start → releaseLocked) 2026-09-26 19:18:42 +03:00
evgeny 0209bcf5d3 android: рация — единый режим per-канал, не гаснет при выходе/заходе в группу 2026-09-26 19:12:07 +03:00
evgeny 44d4265084 ll_queue: queue_waiter_cancel отменяет отложенный call_soon при internal==NULL (фикс UAF waiter_defer) 2026-09-26 18:58:27 +03:00
evgeny f6e6e2c73f stcp: write-error закрывает conn отложенно (фикс UAF keepalive/etcp_tcp_send)
Синхронный send-путь (stcp_link_send → queue_data_put → tx_queue_cb →
flush_pending → try_send) при ошибке сокета вызывал stcp_conn_do_close →
on_close → tcp_link_close_cb → etcp_link_close внутри стека вызывающего,
освобождая ETCP_LINK под keepalive_timer_cb/etcp_tcp_send (use-after-free,
SIGSEGV в keepalive_timer_cb:210).

Теперь write-error планирует отложенный close (uasync_call_soon) с отменой
handle в stcp_conn_do_close; первый err сохраняется; flush_pending не работает
вне STCP_STATE_DATA. Мёртвый hook on_write_error удалён.
2026-09-26 16:42:51 +03:00
evgeny 8aaac45020 ll_queue: отложенный первый запуск автозабора (callback_defer) + тест
queue_set_callback_defer(q, 1) переносит первый запуск callback из
queue_data_put* в uasync_call_soon (очистка стека производителя).
Повторные запуски (queue_resume_callback) и так всегда deferred.
2026-09-26 16:42:46 +03:00
evgeny cc2d2b5f78 etcp: фикс UAF teardown (двойное закрытие линка) + вытеснение down-линка при коллизии адреса
- tcp_link_close_cb (серверная ветка): убрать преждевременный огонь link_status/on_link_down,
  etcp_link_close сам снимает линк из списка и стреляет коллбэки в безопасном порядке
  (устраняет двойное закрытие линка — UAF из callring 45502)
- etcp_connection_close: при callbacks_running откладывать закрытие через uasync_call_soon
  вместо *(int*)0=0
- callbacks_running: uint8_t-флаг -> int-счётчик вложенности (++/--)
- etcp_fire_conn_status / etcp_fire_link_status_cbk: поднимать callbacks_running вокруг огня
- insert_link_queue: при коллизии адреса с чужим conn — ERROR при одинаковом pubkey,
  отказ при up-линке, вытеснение (etcp_link_close) при down-линке

Попутно: доки/комментарии в etcp_router, ACK-интервал 100мс->10мс (ROUTER_ACK_INTERVAL_TB).
2026-09-26 13:40:33 +03:00
evgeny 265421eb96 tun proxy: фикс TCP-суммы (нечётный pbuf), FIN до дренажа to_lwip, tcp_bind/rexmit_fast/active_pcbs_changed + нагрузочный burst-тест 2026-09-25 23:36:45 +03:00
evgeny a20d73d2be proxy/tun: добавить DEBUG-логи диагностики застревания keep-alive (lwIP IN/OUT/RECVED/RTO/PERSIST + per-conn DIAG) 2026-09-25 22:12:47 +03:00
evgeny 4353daf913 freebsd: уникальный dst-адрес per-interface (tunN -> 192.0.2.(N+1)) 2026-09-25 20:16:39 +03:00
evgeny 3a4daf6e77 etcp: строгая последовательность коллбэков при закрытии (NEW→UP↔DOWN→DELETE)
- close: финальный per-connection DOWN только если был UP, до state=2
- etcp_cbk_fire / etcp_fire_conn_status / etcp_link_ready_callback / etcp_on_up: guard state==2
- документация контракта в etcp_api.h / etcp.h
2026-09-25 19:13:38 +03:00
evgeny ab5d1c5f91 topo_group: диагностика senders_list (new_conn/remove_conn) для локализации UAF
- new_conn: DEBUG_INFO ADD с conn/peer/state/ch
- remove_conn: state в enter-логе, детали в !found_in_list WARN, REMOVED-лог
2026-09-25 18:18:39 +03:00
evgeny 08e8b2f1cc route_connectivity: фикс UAF при удалении узла с влётными пробами; отмена TCP-пингов при teardown
- topo_nodeq_remove_node: единый хелпер удаления nq (cancel проб + free paths/subnets + remove + free), заменяет все free-сайты
- route_connectivity_cancel_all: итерация по всем группам + зануление ctx->nq + отмена standby-defer
- etcp_send_tcp_ping: out_cli — хендл stcp_client для досрочной отмены; conn_probe_ctx.tcp_cli
- conn_mgr: cand_tcp_pings[] — отмена влётных candidate TCP-пингов в conn_mgr_destroy
2026-09-25 16:01:19 +03:00
evgeny 3f7b4351cd stcp: фикс UAF log_dump, дренаж очередей при закрытии, защита pre-closed серверной ветки, логирование тихих сбросов rx 2026-09-25 13:49:43 +03:00
evgeny ceaf466b1c chatgui-android: входящий звонок поверх блокировки (inline-оверлей) + вибрация всегда кроме silent 2026-09-24 12:08:49 +03:00
evgeny fb4343fca6 transport: reality-релей idle-timeout (конфиг, дефолт 5с) + warn при >64 релеях; убрать лимит FD_SETSIZE; проверка socket_id в stcp_server 2026-09-24 12:08:45 +03:00
evgeny cc269e810c Звонки: надёжность отмены (INVITE-кодограмма + watchdog) и вибрация входящего по ringer-режиму 2026-09-24 10:15:19 +03:00
evgeny b90dc752d2 call: две фазы исходящего вызова — индикация + разные гудки (desktop/Android) 2026-09-23 19:16:53 +03:00
evgeny 7198154106 chatgui: клик по медиа с отсутствующим файлом — сброс пометки и перекачка 2026-09-22 21:59:59 +03:00
evgeny d237dfec7c diag: кольцевой трейс вызовов teardown + дамп при SIGSEGV (размотка UAF link close) 2026-09-22 20:52:02 +03:00
evgeny 4f29839de5 chat: инвайтер шлёт CONNECT_RESULT(0) после приёма джойнера
- chat_join_process_request: постить CHAT_EVT_CONNECT_RESULT(result=0) в GUI инвайтера
  (иначе диалог «Пригласить в канал» висел на «ожидание ответа»)
- cs_handle_join_request: снять pending_invite_* и погасить invite_send_timer после join
2026-09-22 18:26:44 +03:00
evgeny b46fd599fa chat: push-invite join key + запрет флагов на плейсхолдере
- CHANNEL_INVITE теперь несёт join_key (регистрируется инвайтером локально),
  джойнер шлёт JOIN_INFO_REQ с валидным ключом — чинит автоjoin chatserver-узлов
- ask-путь сохраняет join_key для chat_sync_accept_invite
- chat_member_tags_commit отвергает установку флагов на source!=0 (плейсхолдер)
- member_sync_apply_record: defensive ERROR при adm_tags на плейсхолдере
2026-09-22 17:58:50 +03:00
evgeny 0e625da109 call: ретрансмит INVITE каждые 2с + fail-fast при отсутствии маршрута (6с) 2026-09-22 16:06:52 +03:00
evgeny 93becd36e6 keepalive: TCP-линки без ретраев/ACK; не пинговать спящих пиров, bg-ping цикл ≤ 1/мин 2026-09-22 03:47:50 +03:00
evgeny a3e890b711 keepalive: раздельный sleep-код (announce+ACK-ретраи в окне, dead-link) + без flagless в спячке 2026-09-22 03:29:42 +03:00
evgeny a3df1720b4 keepalive: симметричный sleep-протокол (без is_server) + отдельный ACK-бит 2026-09-22 02:41:15 +03:00
evgeny 331c7849f0 keepalive: убрать периодический keepalive в standby (обе стороны молчат, только фазовые анонсы + один ack) 2026-09-22 02:19:28 +03:00
evgeny 945c6a82b0 keepalive: гвард нулевого тайминга фазы (busy-loop) + разделение echo/announce на сервере 2026-09-22 01:35:25 +03:00
evgeny cc0dcb071e chat: UI настройки звука канала (Android toggle + JNI, desktop через C API) + trampoline 2026-09-22 01:25:55 +03:00
evgeny 3d9c79e752 db_sync: троттлинг push к спящему пиру в тихом чате 2026-09-22 01:21:51 +03:00
evgeny b39b85db13 chat: троттлинг по фазе спячки (member_sync send-only, db_sync sound-aware) + настройка звука 2026-09-22 01:20:32 +03:00
evgeny 9f89fd0f7a transport: keepalive duty-cycle тайминг (active_remaining+sleep_duration) + серверная фаза троттлинга 2026-09-22 01:20:26 +03:00
evgeny 0a1ba79d6f chat: member_sync per-peer throttling on peer sleep (cancel_peer + recv/start gating) 2026-09-22 00:42:24 +03:00
evgeny e4e9fca243 transport: per-peer sleep callback (peer_sleep_cbk) fired from keepalive protocol 2026-09-22 00:40:32 +03:00
evgeny 5628138d16 etcp: keepalive sleep-протокол на флагах (KA_FLAG_SLEEP + sleep_time, announce-retry, ack/echo) 2026-09-21 23:30:39 +03:00
evgeny 8a9d8d0097 fix: утечки памяти в router-сервисах и при shutdown
- conn_mgr_router_recv_handler / md_etcp_recv не освобождали entry (рантайм-утечка на каждое сообщение)
- reality_relay: registry + shutdown hook (отложенный free не отрабатывал при остановке)
- etcp_router: uasync_drain_immediate после destroy (router_close_finalize)
- мелкие callback-списки: activity_cbks, node_cbks, props_cbks/apply_cbks
2026-09-21 21:55:11 +03:00
evgeny 2f3001458f android: standby keepalive sync + rx logging during sleep 2026-09-21 21:17:35 +03:00
evgeny 845af9200d Stop call ring sound immediately on accept 2026-09-21 19:41:21 +03:00
evgeny 328d36376c chat: пишем local_attrs атомарно при вставке своего медиа (гонка с MSG_RECEIVED) 2026-09-21 19:36:07 +03:00
evgeny 6e2a2f0011 media_download: сброс таймера простоя при CONN→REQ + правки диагностики
- md_dl_conn_cb: обнулять last_progress_tb при переходе CONN→REQ, иначе время
  установки indirect-соединения (~2с) засчитывается как простой блока и
  вызывал преждевременный фейловер с держателя, реально имеющего блок
- STALL-лог: печатать миллисекунды корректно (значение в единицах 0.1мс /10)
- media_index: INSERT OR IGNORE — повторная регистрация блока как servable
  больше не шумит как ERROR (UNIQUE constraint)
2026-09-21 19:16:27 +03:00
evgeny 9793733cd5 chatgui: диагностика воспроизведения голосовых сообщений (Windows)
- sound_manager: ma_engine_init/playRawPcm логируют через DEBUG_* (qWarning/qDebug терялись в GUI-приложении без консоли); детальный результат старта PCM (rc/is_playing/cursor)
- main: ранняя инициализация лога в GUI-потоке до SoundManager::init, чтобы диагностика инициализации попала в файл
- messagedelegate: лог клика по Play/timeline (filePath/dlState/существует ли файл)
- voiceplayback: лог заголовка opus (sr/ch/frameSamples)

Попутные правки:
- u_async: фикс двойного close wakeup_pipe (eventfd/Windows)
- chat_core: NULL-guard в get_db/is_initialized
- android: sqlite открывается в utun_instance_init, cleanup stale-потока без утечки fd
- android SettingsScreen: компактная раскладка имени интерфейса
2026-09-21 18:59:04 +03:00
evgeny 19dc076528 chatgui: инициализация call/dm/media backfill при старте узла (fix звонков) 2026-09-21 17:20:41 +03:00
evgeny 6a0373ee73 call: диагностика ставок TX/RX и детект сталла аудио-девайса с хард-ресинком 2026-09-21 16:54:14 +03:00
evgeny 570b421ba0 conn_mgr: parallel reverse+indirect, retransmit, and call teardown fix
- call_on_conn_status: only end call when its own data conn drops
  (INDIRECT call was killed by unrelated topo_group_connect conn DELETE)
- conn_mgr: run REVERSE and INDIRECT in parallel after DIRECT fails;
  reverse is preferred (indirect defers via indirect_ready until reverse
  resolves). REVERSE timeout 15s -> 3s.
- retransmit DIRECT_REQ / EXCHANGE_REQ every 1s (idempotent, same request_id)
  to survive packet loss (router RST dropping stale-channel requests)
- replace main.phase with reverse_active/indirect_active/indirect_ready flags
- add diagnostics to trace the parallel flow
2026-09-21 15:22:16 +03:00
evgeny ee54bd52b3 conn_mgr: fix reverse-phase timer leak causing use-after-free crash
cm_ncd_callback re-entered cm_start_phase_reverse on every NCD timeout
during REVERSE phase (shared NCD revived by other modules), leaking reverse
timers: only the last one was tracked in entry->main.timer, so cm_entry_cleanup
cancelled only that one and the earlier timers fired on the freed entry.

- ignore repeated NCD timeouts while already in REVERSE (phase==2)
- cancel entry->main.timer before overwriting in reverse/indirect phases
- remove entry from reverse_pending in cm_entry_cleanup
2026-09-21 13:57:32 +03:00
evgeny e885fe30f5 chatgui: remove universal black background from video window (fix black boxes around controls) 2026-09-21 12:50:06 +03:00
evgeny cd0757e272 chatgui: declare metatype for shared_ptr<vector<int16_t>> (Qt5 build fix) 2026-09-21 12:42:21 +03:00
evgeny f5fece4959 chatgui: fix video playback on Windows (metatype registration + swscale YUVJ range) 2026-09-21 12:39:18 +03:00
evgeny 01987a75d0 chatgui: verify FFmpeg DLL deployment + fix dependency resolution order 2026-09-21 11:52:21 +03:00
evgeny 9d4538935d chatgui: restore FFmpeg build script (build_ffmpeg.sh + .gitignore)
Скрипт был случайно удалён в d773f172 вместе с .gitignore/.opencode.
Без него на Windows не собирается ffmpeg/ffmpeg_build/install -> HAVE_FFMPEG
не определяется -> воспроизведение и транскодирование видео отключены.
2026-09-21 11:29:47 +03:00
evgeny 6ee9bad17d chat: defer media auto-download to per-channel backfill after sync
- db_sync insert-cb gets initial_sync flag (SEND_DATA=1, PUSH/local=0)
- on_msg_inserted skips media download during initial sync
- new chat_media_autodownload_backfill_channel (newest-first) triggered
  on SYNC_DONE per channel (2s debounce)
2026-09-21 02:07:24 +03:00
evgeny a5047c29f7 chatgui: always deploy non-Qt DLLs (FFmpeg, mingw-runtime) next to vibechat.exe 2026-09-21 00:58:56 +03:00
evgeny 2f1545de38 chatgui: fix Qt5 build — QMouseEvent/QWheelEvent position() -> localPos()/posF() 2026-09-21 00:51:48 +03:00
evgeny de850db548 chat: remove join_password check from per-channel JOIN_INFO_REQ (server-link only) 2026-09-20 22:23:42 +03:00
evgeny 39a1da4c28 chat: fix use-after-free in invite_candidates trampoline + accept JOIN_INFO_RESP/READY/ERROR for unknown channels 2026-09-20 22:03:31 +03:00
evgeny d1432b7d19 tests: fix Windows build — SKIP fork-based e2e, cross-platform mkdtemp/setenv 2026-09-20 20:21:50 +03:00
evgeny a7a3666d85 radio: AGC release-rate (без кликов), jitter dead-zone, TX/RX-статистика, FGS-гонка и кэш настроек 2026-09-20 20:07:48 +03:00
evgeny ba1c2d85d3 reality_relay: async DNS вместо getaddrinfo + AGC-компрессор по умолчанию (-12 dBFS) 2026-09-20 15:28:46 +03:00
evgeny 738cfe7835 radio: адаптивный джиттер-буфер (pre-roll, симметричный темп, reorder) + AGC-компрессор микрофона
- voice_jitter: vj_create_cfg (target_ms/min_tempo/max_tempo/pre_roll), pre-roll
  с таймаутом, vj_end для дренажа остатка, глубина учитывает внутренний буфер SoundTouch
- radio_audio: reorder по seq (mesh, out-of-order через соседей), force-flush pending
  по таймауту и FIN, компрессор микрофона (AGC, конфиг [chatserver]), статистика буфера
- audio_compressor: clear_output для потокового слива без потери gain_smoothed
- chatgui messagelist: fromUtf8 для emoji-строк
- тесты: pre-roll/симметричный темп, reorder, late-drop
2026-09-20 14:39:25 +03:00
evgeny 27e66f1092 log: move socket diagnostics SYS->SOCKET and conn lifecycle ETCP->CONNECTION (avoid poll() flood) 2026-09-20 01:51:40 +03:00
evgeny 1bde2eca35 diag: log conn add/remove/free pointers to pinpoint BGP senders_list UAF 2026-09-20 01:42:05 +03:00
evgeny 16237b0df8 etcp_send: fix contract (caller owns entry on error) + fix 7 leak sites 2026-09-20 01:35:30 +03:00
evgeny fae0a7c069 uasync: cast function pointers in socket_add log to void* 2026-09-20 01:18:33 +03:00
evgeny b33caa56b8 uasync: pass socket name to all add_socket call sites 2026-09-20 01:13:38 +03:00
evgeny c36cebe357 uasync: add socket name diagnostics and HUP/ERR auto-unregister safety net 2026-09-20 01:11:30 +03:00
evgeny 90520a83ff topo_group_connect: не подключаться к самому себе в авто-цикле
Phase 1/2/3 и on_up/on_down не фильтровали self: узел попадал в список
connected-пиров (get_connected_peers не исключает self) и разворачивал
node_conn_direct_open на собственные адреса (12 линков на 5C31->5C31).
Ручной путь (connect_node_once) фильтр уже имел.

Добавлен skip self в Phase1 init/cycle_restart, Phase2, Phase3 и в
on_up/on_down (self не помечается connected и не считается в active_conn_count).
2026-09-19 23:40:06 +03:00
evgeny 6b92964794 radio/android: PTT FGS microphone permission + детект потери микрофона
- FOREGROUND_SERVICE_MICROPHONE запрос + foregroundServiceType microphone|specialUse (Android 14+)
- RadioOverlayService.start: не стартовать без mic-разрешения, ServiceCompat.startForeground с TYPE_MICROPHONE
- RadioAudioEngine: micDead-флаг и лог при обрыве captureLoop/read
- radio.c: лог первого кадра burst на транзитных узлах
- SettingsScreen: категория radio в списке debug-категорий
2026-09-19 23:13:04 +03:00
evgeny 688785fc6e etcp: фикс зависшего reinit_pending после реинита готового соединения
etcp_conn_queue_set_ready() делает ранний выход по state!=0, поэтому при
реините уже готового соединения (peer restart / duplicate INIT / collision)
reinit_pending оставался в 1 навсегда. Добавленные в 841b3042 guard'ы
etcp_conn_input/etcp_output_try_assembly дропали все входящие пакеты.

Снятие reinit_pending + REINIT перенесено в etcp_conn_ready.
2026-09-19 23:13:04 +03:00
evgeny 5e955168d2 chatgui-android: PTT-оверлей — talking flow, цвет idle/talking, drag-lock
- RadioAudioEngine: StateFlow talking для реактивной подсветки кнопки
- RadioOverlayService: градиент idle/talking, drag-lock, сброс isShown при destroy
- ChatScreen: скрывать PTT-кнопку при активном оверлее, подсветка по talking
2026-09-19 21:17:38 +03:00
evgeny 841b3042cf etcp: фикс double-free буферов паковщика и повторных free после неудачного queue_data_put
- pn_send_to_etcp/pn_unpacker_cb: занулять pn->data/pn->recvpart ДО queue_data_put
  (ре-энтрантный close/reinit в синхронной цепочке освобождал буфер повторно)
- etcp_output_try_assembly: гард state==2/reinit_pending + убрать re-put освобождённого entry
- etcp_conn_input: гард ре-энтрантного reinit/close в цикле секций, локальный etcp вместо pkt->link
- убраны повторные free после queue_data_put (он сам освобождает entry+dgram) в 6 местах
- диагностика: obj=%p в DOUBLE FREE, логи free dgram в pn_deinit/ack_recv
2026-09-19 21:17:33 +03:00
evgeny 4a785772be chatgui-android: PTT по volume-кнопкам (вверх/вниз) + синхронизация оверлея
- volume up/down обе держат PTT (holdCount), настройка radio_volume_ptt
- syncRadioOverlay при включении/выключении рации
2026-09-19 18:16:46 +03:00
evgeny 8a9184675a radio: дедупликация дублей в mesh + фикс OOB в hop_list
- radio_recv: кэш (src,stream,seq) с TTL 2с гасит кадры, пришедшие по второму пути (diamond/mesh)
- фикс записи за границу new_hops[hop_count] при hop_count==RADIO_MAX_HOPS
- счётчик c_dup_dropped + radio_dup_dropped() для диагностики
- тест test_radio_mesh (4 узла, diamond): C получает ровно N кадров, dup>0
2026-09-19 18:16:42 +03:00
evgeny b96ed8bd00 radio: PTT-радио в каналах (src/radio + chatgui + chatgui-android)
Новый модуль src/radio (radio/radio_audio/radio_headless) — push-to-talk
радио поверх topo-групп, доставка аудио в канал, headless-режим и тесты.
Интеграция в chatgui (ptt_hotkey_manager, radio_audio_engine, радионастройки,
вызовы) и chatgui-android (RadioAudioEngine, RadioOverlayService, UI).
2026-09-19 17:09:00 +03:00
evgeny 398078a28f headless/call: закрывать клиента и логировать при ошибке send() (EPIPE/ECONNRESET) 2026-09-19 10:43:04 +03:00
evgeny 6c42390fc2 utun: игнорировать SIGPIPE, чтобы send() в закрытый TCP-сокет не убивал демон 2026-09-19 10:32:57 +03:00
evgeny 1de1409699 conn_mgr: handle-based API (conn_mgr_open/close), фазы DIRECT/REVERSE/INDIRECT, тесты phases+handles 2026-09-19 09:31:08 +03:00
evgeny 734a414186 etcp: единый реестр svc_id + убрать коллизию CHAT/CONN_MGR (0x11)
Все ETCP_ID_*/ETCP_RT_ID_* сведены в etcp_api.h (единственное место
определения) с реестром и предупреждением про коллизии в пределах одного
пространства имён. Убраны локальные define из topo_group.h,
topo_group_invite.h, db_sync.h, chat_sync.h, chat_join.h; хардкод 0x31 в
member_sync.c заменён на ETCP_RT_ID_MEMBER_SYNC.

Коллизия: chatgui-десктоп биндил мёртвый ETCP_RT_ID_CHAT 0x11 поверх
ETCP_RT_ID_CONN_MGR 0x11 — затирался conn_mgr_router_recv_handler, из-за
чего INDIRECT-обмен conn_mgr не работал и голос звонка не устанавливался.
Удалён мёртвый код recvCallback/messageReceived/send в utun_node.{cpp,h}.
2026-09-19 00:18:39 +03:00
evgeny 6d3ad965ee topo: remember REQUEST_TABLE for not-yet-created groups, fulfill on create
При REQUEST_TABLE для ещё не созданной группы (CHAT-канал не загружен) пир
дропал запрос, и проситель навсегда не узнавал таблицу — односторонняя
видимость (звонок android→linux падал «peer offline»). Теперь запрос
запоминается в pending_table_reqs[32] и выполняется при создании группы
(topo_group_fulfill_table_reqs → handle_request_table).

Попутно: test_chat_join_e2e — критерий успеха через CHAT_EVT_CONNECT_RESULT
(JOIN_READY) вместо senders_list; doxygen-комментарии в etcp_api.h.
2026-09-18 23:36:45 +03:00
evgeny 8bfb456161 etcp: fix OOB/DoS in packet parsing and ping path
- FILLER: drop (uint16_t) truncation in fill_len bound (OOB pointer)
- ACK: require len >= ETCP_ACK_BASE_SIZE (8); wrap-aware clamp of till
  to next_tx_id + 16MB cumulative-ACK iteration limit (DoS)
- ping: bound len before sc_encrypt (stack overflow), ulen bound,
  allocate ETCP_DGRAM + PACKET_DATA_SIZE (heap overflow)

Signed-off-by: uTun <dev@utun>
2026-09-18 17:58:40 +03:00
evgeny 1340ab123b reality/stcp: fix OOB read in key_share parse, padding clamp, shbuf leak
- reality_parse_client_hello: bound shares_len by extension data length
  (len-2) to prevent stack-buffer-overflow from malformed ClientHello
- stcp handshake: reject padding_size > STCP_HS_PADDING_MAX (512)
- reality_ch_body_cb: free shbuf on stcp_try_send error (leak)
- tests: fuzz key_share parse; raw-socket handshake control/clamp/garbage

Signed-off-by: uTun <dev@utun>
2026-09-18 17:20:19 +03:00
evgeny e7fc69bb1d topo: комментарии к функциям topo_node/topo_group
conn_mgr: src node_id вместо conn->peer_node_id, препенд ETCP_RT_ID_CONN_MGR к dgram, bind CONN_MGR после etcp_router_init

call: guard CALL_ST_CONNECTING при accept
2026-09-17 17:30:05 +03:00
evgeny 1fce405204 debug: вычистить логи из категории DEBUG_CATEGORY_DEBUG (удалить дампы, перекатегоризовать ERROR-проверки в ETCP) 2026-09-17 00:17:32 +03:00
evgeny e6a4ffd418 reality: fix leak of ClientHello init buffer in reality_server_start_relay 2026-09-17 00:05:42 +03:00
evgeny 1f1df4f437 mem: детект аномально больших аллокаций (LARGE ALLOC log + calloc overflow guard) 2026-09-16 21:12:46 +03:00
evgeny 51a5c5bcbf router: fix UAF on rconn->recv_conn after ETCP_CONN destroy
recv_conn хранил заимствованный указатель на физический conn без очистки
при его освобождении. Добавлен etcp_router_conn_destroyed(), вызываемый из
etcp_connection_close фазы 1; RST теперь отправляется через живой conn,
доставивший пакет, а не через устаревший recv_conn.
2026-09-16 20:27:03 +03:00
evgeny 51d6d22a27 sock_match: классификация сокета по адресу, а не по type из конфига (fix Q8/Android→LAN)
ip_is_public() вынесен в lib/platform_compat (единый классификатор IPv4 с ntohl).
auto_socket_reconcile_config: тип сокета выставляется по фактическому адресу.
sock_collect_views: has_priv/has_inet/is_eim — по классификации адреса и NAT-детекции.
is_local_subnet упрощён до !ip_is_public(). Тест test_sock_match обновлён.
2026-09-16 20:26:55 +03:00
evgeny 21d9a4e5b7 dm: заполнять peer_name при автосоздании беседы (иначе в списке 0x...) 2026-09-16 20:26:55 +03:00
evgeny 5293fdd3a9 call(android): звать call_audio_init перед start; логировать feed_pcm без instance
Без call_audio_init(inst) g_inst оставался NULL и call_set_audio_ops не
регистрировался: TX (feed_pcm) падал на !ua, RX (audio_ops->on_media) не
вызывался — тишина и спам 'callAudioFeed returned false'. Зеркалим desktop:
utun_bridge_call_audio_start теперь делает call_audio_init + call_audio_start.
2026-09-16 20:26:55 +03:00
evgeny 2de554f8b9 call: единый голосовой стек (libutun_voice) + SoundTouch на Android
- voice_jitter (кодированные Opus-кадры + decode-on-pull + SoundTouch time-stretch)
  вместо PCM-буфера audio_jitter; излишек держится до декодирования
- call_audio.c/.h — единая реализация для desktop/Android/headless (C-интерфейс)
- libutun_voice (voice_sources.cmake): SoundTouch + voice_jitter + call_audio + call_tones
- Android: pull-модель (nativeCallAudioPull), убран Kotlin FIFO, ANDROID_STL=c++_static
- headless audio-сокет: PCM-протокол (decode-only, без тактирования), daemon остаётся C-only
- CALL_STATS расширен dropped/underruns; удалены audio_jitter.cpp и libutun_lite/call_audio.c
- тест test_voice_jitter (ёмкость/догон/стационар), test_call_headless переведён на PCM
2026-09-16 20:26:55 +03:00
evgeny de930b21a2 ncd: fix UAF on deferred UP delivery after handle close
ncd_deliver_up_cb (uasync_call_soon) держал сырой указатель handle h,
который мог быть освобождён node_conn_direct_close/force_close раньше
выполнения отложенного callback'а. Сохраняем токен call_soon в handle
и отменяем его при close через uasync_call_soon_cancel.
2026-09-16 20:26:55 +03:00
evgeny e4557b0473 dm: маршрутизация через общую группу (group_id), JSON-эскейпинг; звонки: иконки/UI в desktop и android 2026-09-16 15:05:48 +03:00
evgeny b483335c06 bgp/ncd: владение conn через NCD-handle в senders_list; call audio route persist + incoming call sound 2026-09-16 13:29:28 +03:00
evgeny b11e77e4c0 call: P2P аудиозвонки — сигналинг, релей медиа, аудио-контур, headless/desktop/android
- src/call/: сигналинг (call.c, call_proto.h, ETCP_RT_ID_CALL 0x36) + релей медиа
  + headless аудио-сокет (call_headless.c, call_audio_bind) + Opus/jitter/тоны
- chat: события CHAT_EVT_CALL_* (34–40), CHAT_EVT_DM_MESSAGES (41), DM-трамплины
- chatgui: call window, miniaudio I/O, audio_frame_ring, test_audio_jitter
- chatgui-android: CallScreen/IncomingCallDialog, аудио-роутинг, JNI, рингтон
- tests: test_call, test_call_headless
2026-09-16 10:17:57 +03:00
evgeny 0157f7f96b build: fix --asan to build lib/libopus dependency 2026-09-15 22:44:27 +03:00
evgeny 21ec740d82 socks_proxy: defer conn free to fix UAF from synchronous write_queue flush 2026-09-15 22:39:27 +03:00
evgeny b085715654 tun/nat: fix UAF при teardown и use-after-free в tun_read_packet 2026-09-15 22:07:55 +03:00
389 changed files with 33584 additions and 8117 deletions

No files matched your search

+1
View File
@@ -3,6 +3,7 @@
build-win/
tools/chatgui/build/
tools/chatgui/ffmpeg/ffmpeg_build/
tools/lightsout/build/
# Build outputs
*.o
+130 -86
View File
@@ -34,10 +34,10 @@ This file contains essential information for AI coding agents working in the uTu
## Quick Reference
**Repository:** uTun - Secure VPN tunnel with ETCP protocol
**Repository:** uTun — общее сетевое ядро ETCP, независимо запускаемые UTUN и P2P-чат
**Language:** C (C99)
**Build System:** GNU Autotools (autoconf/automake)
**Cryptography:** OpenSSL (AES-CCM, X25519, SHA256)
**Cryptography:** OpenSSL (AES-CCM, X25519, Ed25519, SHA256)
## Build Commands
@@ -68,9 +68,10 @@ make distclean # Clean everything including configure files
### Partial Builds
```bash
cd lib && make # Build only the library (libuasync.a)
cd src && make # Build only the main program
cd tests && make # Build only tests
make -C lib # базовые библиотеки
make -C lib/libopus # встроенный Opus
make -C src # основной бинарник src/utun (библиотеки уже должны быть собраны)
make -C tests # тесты (используют объекты src/)
```
### Direct Build (Windows, без autotools)
@@ -86,7 +87,7 @@ python3 tools/chatcli members <ch_id> # мемберы с полн
python3 tools/chatcli messages <ch_id> [count] # последние сообщения
python3 tools/chatcli send <ch_id> "text" # отправить сообщение
python3 tools/chatcli invite <ch_id> # создать invite-ссылку
python3 tools/chatcli connect <utun://...> # join по ссылке
python3 tools/chatcli connect 'utun://...' # join по ссылке
python3 tools/chatcli create "Name" # создать канал
python3 tools/chatcli listen # слушать события (Ctrl+C выход)
```
@@ -94,13 +95,14 @@ python3 tools/chatcli listen # слушать событи
### ASAN Build (AddressSanitizer, Linux)
```bash
./build.sh --asan -j4 # lib/ + src/ only (tests not ASAN-compatible)
./build.sh --asan -j4 # собирает lib/ + src/; тесты этот режим скрипта пропускает
```
Запуск с ASAN (detect_leaks отключены чтобы не спамить при shutdown):
```bash
ASAN_OPTIONS=detect_leaks=0:halt_on_error=0 ./src/utun -f -p /tmp/utun.pid -l /tmp/utun.log
ASAN_OPTIONS=detect_leaks=0:halt_on_error=0:log_path=/tmp/utun_asan ./src/utun -f -p /tmp/utun.pid -l /tmp/utun.log
```
Краш-логи ASAN пишутся в `/tmp/utun_asan.<pid>`.
В этом примере отчёты ASAN пишутся в `/tmp/utun_asan.<pid>` благодаря `log_path`.
Для отдельного теста нужно собирать его зависимости с теми же sanitizer-флагами; обычные и ASAN-объекты не смешивать.
## Test Commands
@@ -118,13 +120,13 @@ cd tests/
./test_etcp_two_instances
```
### Run Single Test with Debug Info
### Build and Run a Single Test
```bash
cd tests/
gcc -I../src -I../lib -I../tinycrypt/lib/include \
-o my_test test_file.c ../src/*.c ../lib/*.c ../tinycrypt/lib/source/*.c
./my_test
make -C tests test_services
cd tests && ./test_services
```
Использовать цель из `tests/Makefile.am`: ручная компиляция `src/*.c` пропускает подкаталоги и добавляет лишний `main`.
Уровни и категории диагностики настраиваются в самом тесте через `debug_set_level` / `debug_set_category_level`.
## Code Style Guidelines
@@ -137,7 +139,7 @@ gcc -I../src -I../lib -I../tinycrypt/lib/include \
### Formatting
- **Indentation:** 4 spaces, no tabs
- **Braces:** Same line for functions, new line for control structures:
- **Braces:** Открывающая скобка на строке функции или условия:
```c
int function(void) {
if (condition) {
@@ -145,7 +147,7 @@ gcc -I../src -I../lib -I../tinycrypt/lib/include \
}
}
```
- **Comments:** Primary language is Russian for business logic, English for API docs
- **Comments:** Краткие описания на русском; сохранять принятые имена API и протокольных полей.
- **Line Length:** Aim for 80-100 characters, but can go up to 150 if logically coherent
### Include Order
@@ -201,7 +203,7 @@ sc_context_t ctx;
sc_init_ctx(&ctx, &my_keys);
// 2. Set peer public key (for key exchange)
sc_set_peer_public_key(&ctx, peer_public_key, SC_PEER_PUBKEY_HEX); // 0=bin, 1=hex
sc_set_peer_public_key(&ctx, peer_public_key_bin, SC_PEER_PUBKEY_BIN); // 32 бинарных байта; HEX — для hex-строки
// 3. Ready for encrypt/decrypt
sc_encrypt(&ctx, plaintext, plaintext_len, ciphertext, &ciphertext_len);
@@ -227,40 +229,66 @@ debug - отладка: всё что нужно для понимание су
info - сообщения для пользователя о работе. мы должны видеть в читаемом виде понятные и осмысленные сообщения о каких-либо значимых действиях с точки зрения логики работы модуля или приложения
warn / error - ошибки и предупреждения (аномалии). должны быть во всех ошибочных ветках. Молча вываливаться с ошибкой нельзя, все ошибки и предупреждения должны логироваться.
### Debug Categories (28 категорий)
```
NONE=0, UASYNC=1, LL_QUEUE=2, CONNECTION=3, ETCP=4, CRYPTO=5, MEMORY=6,
TIMING=7, CONFIG=8, TUN=9, ROUTING=10, TIMERS=11, NORMALIZER=12, BGP=13,
SOCKET=14, CONTROL=15, DUMP=16, TRAFFIC=17, DEBUG=18, GENERAL=19, NAT=20,
KEEPALIVE=21, ETCPROUTE=22, BBR=23, ETCP_DUMP=24, CONNECTIVITY=25, DB_SYNC=26,
MEMBER_SYNC=27
```
### Debug Categories
Актуальные имена и константы — в `lib/debug_config.h`, текстовые имена — в `lib/debug_config.c`.
Основные: `sys`, `connection`, `etcp`, `crypto`, `config`, `tun`, `routing`, `timers`, `bgp`,
`socket`, `control`, `dump`, `traffic`, `debug`, `general`, `nat`, `keepalive`, `etcp_route`,
`media`, `etcp_dump`, `chat`, `chat_sync`, `member_sync`, `proxy`, `video`, `reality`, `dm`, `call`, `radio`.
UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в ETCP; старые числовые списки не использовать.
### Настройка отладки
- В конфиге: `debug = etcp=trace,config=info` (формат: `категория=уровень,...`)
- В INI-конфиге: секция `[debug]`, каждая категория отдельной строкой (`etcp=trace`, `config=info`).
- Строку `etcp=trace,config=info` принимает `debug_parse_config()`; GUI хранит её в `[gui] debug_categories`.
- В коде: глобальный уровень и per-category уровни из `debug_config_t g_debug_config`
- Макросы: `DEBUG_ERROR(cat,fmt,...)` `DEBUG_WARN` `DEBUG_INFO` `DEBUG_DEBUG` `DEBUG_TRACE`
- `log_dump(prefix, data, len)` — hex dump в лог
- `log_dump(level, category, prefix, data, len)` — hex dump в лог
### Dual Output
- Консоль и файл настраиваются раздельно (`debug_set_console_level`, `debug_set_file_level`)
- Фильтр уровней общий для выходов; `debug_enable_console()` включает/выключает консоль.
- `debug_enable_file_output(path, truncate)` / `debug_disable_file_output()`
## Architecture
### Ядро, сервисы и владение ресурсами
- `utun_instance_create*()` создаёт ядро; `utun_core_start()` запускает общие сетевые службы.
- `utun_service_start/stop()` управляет UTUN-группой, TUN/NAT и запросами к пирам из конфига.
- `chat_service_start/stop()` управляет чатом и CHAT-группами. Чат не требует UTUN; desktop и Android запускают ядро и чат явно.
- Ядро владеет общей SQLite (`db_path/chats.db`, без пути — `:memory:`), идентичностью, транспортом и маршрутизатором.
Остановка сервиса сохраняет БД и ресурсы другого сервиса. `utun_instance_destroy()` освобождает ядро, но не переданный `UASYNC`.
- Группа владеет NCD handle с начала CONNECTING. Инициатор хранит отменяемый `TOPO_PEER_REQUEST`;
закрытие своего запроса не разрывает транспорт других владельцев. Готовая сессия сохраняется у группы.
- UP означает работоспособный транспорт. READY означает завершённое согласование и обмен таблицей конкретной группы.
Recovery ждёт нужных маршрутов через READY-пиров; наличие `ETCP_CONN*` само по себе не гарантирует UP.
- JOIN добавляет подписанного мембера, Merkle распространяет запись. Доступ к CHAT-группе каждый узел даёт по своей таблице участников;
invite-ключ не даёт временного членства. Протокол описан в `src/chat/chat_join.h`, групповое согласование — в `src/routing_layer/topo_group.h`.
- `TOPO_NODE` — общая подписанная запись узла. Более свежий timestamp заменяет запись целиком, с полями и подписью;
адреса не версионируются отдельно. BGP обновляет запись и передаёт изменения NCD (см. `doc/node_snapshot.md`).
- BGP хранит один путь к назначению от каждого соседа. WITHDRAW удаляет только путь отправителя;
NODEINFO с прежним timestamp также обновляет путь. Выбор: живой путь с минимумом хопов, затем меньший ID соседа.
REINIT сохраняет пути до проверки нового снимка; TABLE_COMPLETE удаляет неподтверждённые, зависший обмен перезапускается.
- ETCP-router дописывает ID при транзите и отбрасывает повторное посещение для DATA/ACK/control.
Список переходов находится вне подписанного тела; формат описан в `src/routing_layer/etcp_router.h`.
Все операции с ядром выполняются в его uasync-потоке; stop/destroy — вне callbacks останавливаемых сервисов.
Порядок освобождения ресурсов и нюансы media workers: `doc/service_lifecycle.md`.
### Directory Structure
```
├── lib/ # Core libraries (18 .c + 20 .h + libopus/liblmdb)
├── lib/ # Core libraries, SQLite, libopus/liblmdb
├── src/ # Main source code
│ ├── transport_layer/ # ETCP stack, crypto, STCP, normalizer, BBR (~30 файлов)
│ ├── routing_layer/ # Routing, BGP, conn_mgr, etcp_router, NAT detection (~20 файлов)
│ ├── chat/ # P2P чат, db_sync, merkle_sync, member_sync (17 файлов)
│ ├── proxy/ # SOCKS5, TCP/UDP/ICMP прокси (10 файлов)
│ ├── lwip_tcp/ # Встроенный lwIP TCP стек (7 файлов)
│ ├── media_delivery/ # Доставка медиафайлов (6 файлов)
│ ├── media_async/ # Асинхронный движок медиа (2 файла)
│ └── *.c/h # Корневые модули: utun, config, TUN, firewall, NAT, NTP, control (~14 файлов)
├── tests/ # ~70 тестов, 64 в check_PROGRAMS
│ ├── transport_layer/ # ETCP, crypto, STCP, normalizer, NCD, BBR
│ ├── routing_layer/ # Routing, группы/BGP, recovery, conn_mgr, etcp_router
│ ├── chat/ # P2P чат, join, db_sync, merkle_sync, member_sync
│ ├── dm/ # Личные сообщения и mailbox
│ ├── call/ # Звонки и общий голосовой стек
│ ├── radio/ # Групповая PTT-рация
│ ├── proxy/ # SOCKS5, TCP/UDP/ICMP прокси
│ ├── lwip_tcp/ # Встроенный lwIP TCP стек
│ ├── media_delivery/ # Доставка медиафайлов
│ ├── media_async/ # Фоновые задачи медиа
│ └── *.c/h # utun, config, TUN, firewall, NAT, NTP, control
├── tests/ # Состав тестов и цели запуска: tests/Makefile.am
├── doc/ # Technical Specifications
├── tools/ # Auxiliary tools
│ ├── etcpmon/ # GUI монитор ETCP
@@ -278,7 +306,7 @@ MEMBER_SYNC=27
**Core (src/)**
- `utun.c` - Main program entry point, CLI parsing, daemon mode
- `utun_instance.c/h` - Root instance lifecycle, config loading, all submodule init
- `utun_instance.c/h` - Создание ядра и независимый жизненный цикл UTUN/чата, владение общими ресурсами
- `tun_if.c/h` - TUN interface API (init/write/close, cross-platform)
- `tun_linux.c` `tun_freebsd.c` `tun_windows.c` - Platform-specific TUN implementations
- `tun_route.c/h` - TUN routing table sync
@@ -314,7 +342,7 @@ MEMBER_SYNC=27
- `stcp_server.c/h` - STCP сервер
- `stcp_client.c/h` - STCP клиент
- `stcp_link.c/h` - STCP линк-уровень
- `node_conn_direct.c/h` - Direct config-based connections
- `node_conn_direct.c/h` - Общий прямой транспорт к пиру с отдельным handle каждого владельца; обновление линков по адресам узла
- `socket_monitor.c/h` - Socket state monitoring
- `reality.c/h` - REALITY-style TLS ClientHello/ServerHello камуфляж для STCP
- `reality_fingerprint.c/h` - Статические отпечатки (cipher suites, groups, ALPN) для REALITY-камуфляжа
@@ -329,28 +357,30 @@ MEMBER_SYNC=27
- `route6_lib.c/h` - IPv6 routing
- `route_ping.c/h` - Route ping probing (NAT check, liveness)
- `route_connectivity.c/h` - Route connectivity checks
- `topo_group.c/h` - BGP-like route exchange between nodes (NODEINFO/WITHDRAW, изолированные группы UTUN/CHAT)
- `topo_group_connect.c/h` - Авто-подключение к узлам CHAT-группы (3 фазы, бесконечный цикл)
- `topo_group_invite.c/h` - Invite/join канала через прямое ncd-подключение (INVITE_INFO_REQ/RESP)
- `topo_group.c/h` - Сессии UTUN/CHAT (JOIN/ACCEPT, эпохи, READY), групповые запросы, NODEINFO/WITHDRAW и владение NCD
- `topo_group_connect.c/h` - Подбор пиров CHAT-группы: исторические → суперузлы/публичные → локальные; отменяемые запросы до READY
- `topo_group_invite.c/h` - Получение описания канала через INVITE_INFO_REQ/RESP; добавление мембера реализовано в chat_join/chat_sync
- `topo_node.c/h` - Node management (модель узла: TOPO_NODE/TOPO_GROUP_NODE, сериализация, node_registry)
- `topo_node_sqlite.c/h` - SQLite persistence for nodes
- `topo_recovery.c/h` - Восстановление каскадно отвалившихся узлов после разрыва соединения
- `topo_recovery.c/h` - Восстановление потерянных маршрутов группы через READY-сессии, отмена попыток при stop
- `conn_mgr.h` + `conn_mgr_core.c`/`conn_mgr_indirect.c`/`conn_mgr_monitor.c` + `conn_mgr_priv.h` — Connection Manager (handle-based API, 3-phase DIR/REV/IND)
- `etcp_router.c/h` - ETCP router/multiplexer (мультиплексирование каналов, transit forwarding)
- `nat_detection.c/h` - NAT type detection
- `route_crypto.c/h` - Пред/пост-обработка SVC_ROUTE пакетов (encrypt-then-sign)
**Chat (src/chat/)** — децентрализованный P2P чат: сообщения и участники автоматически синхронизируются между всеми узлами канала без центрального сервера.
- `chat_core.c/h` - Главный модуль чата. Единственная точка входа из GUI: принять сообщение, создать канал, изменить настройку. Управляет БД и жизненным циклом всех chat-модулей
- `chat_core.c/h` - API сообщений, каналов, профиля и настроек; chat_service_start/stop управляют чат-сервисом поверх общей БД ядра
- `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` - Автоматическая репликация данных между подключёнными узлами. Запись на одном узле — появляется у всех остальных. Криптоподписи и цепочка хешей защищают от подделок
- `chat_setting.c/h` - Проверка и хранение настроек чата per-instance; общая логика для конфигурации и GUI
- `chat_join.c/h` - Регистрация invite-ключа с ACK, пересылка запроса инвайтеру и добавление подписанного мембера; контракт join в заголовке
- `db_sync.c/h` - Репликация подписанных записей SQLite через прямые ETCP-соединения; расхождения по цепочке хешей, новые записи через PUSH
- `member_sync.c/h` - Синхронизация участников каналов поверх merkle_sync. Два подписанных блока (мембер/владелец), по-блочное сравнение версий, верификация подписей. Удаление — только битая запись
- `merkle_sync.c/h` - Универсальный движок синхронизации на Merkle-деревьях (5 уровней × 32 бакета, SHA256). Пиры обмениваются хешами и передают только различия; per-session out-очередь с backpressure. Не привязан к конкретному типу данных (коллбэки `data_ops`)
- `merkle_tree.c/h` - Производный индекс Merkle-хешей в SQLite; восстанавливается из данных модели, обновляется с ними в одной транзакции
- `chat_profile.c` - Собственный профиль: имя узла, сетевые адреса, сохранение UI-состояния (часть chat_core)
- `chat_channel.c` - Создание и настройка каналов: готовит таблицы в БД, генерирует криптоключи, запускает подключение ко всем участникам (часть chat_core)
- `chat_channel.c` - Создание и настройка каналов: таблицы, криптоключи, CHAT-группа и планировщик подключения (часть chat_core)
- `chat_msg.c` - Отправка и приём сообщений: запись в локальную БД, автоматическая рассылка всем участникам канала (часть chat_core)
- `chat_status.c` - Сбор диагностики: текущее время, активные соединения, типы NAT. Отправляется в GUI (часть chat_core)
- `chat_member.c/h` - Единая структура мембера для отображения в GUI (десктоп/headless/Android), фиксированная сериализация на проводе
@@ -358,7 +388,7 @@ MEMBER_SYNC=27
- `chat_whisper.c/h` - Whisper speech-to-text транскрипция голосовых сообщений (опционально, асинхронно через media_async)
- `chat_headless_control.c/h` - TCP control-socket для headless chat CLI (JSON line protocol)
- `invite_build.c/h` - Сборка invite-ссылок utun:// (выбор лучшего узла + адреса)
- `invite_link.c/h` - Формат invite-ссылок utun:// для каналов (base64: version|password|channel_id|pubkey|addrs)
- `invite_link.c/h` - Кодирование utun://: версия, пароль, channel_id, join_key, Reality-параметры, ключ и адреса узла
**Прокси (src/proxy/)**
- `socks_proxy.c/h` - SOCKS5 прокси
@@ -379,11 +409,11 @@ MEMBER_SYNC=27
- `media_index.c/h` - Индексация медиафайлов
**Media Async (src/media_async/)**
- `media_async.c/h` - Асинхронный движок (thread-per-task для crypto/file операций)
- `media_async.c/h` - Фоновые crypto/file-задачи: work в pthread, done в uasync; destroy ждёт workers и завершает ожидающие callbacks с CANCELLED
**Libraries (lib/)**
- `u_async.c/h` - Async event loop (epoll/poll/select, timers via timeout_heap)
- `ll_queue.c/h` - Lock-free linked list queue with callbacks, hash index, threshold waiter
- `ll_queue.c/h` - Двусвязная очередь одного uasync-потока с callbacks, хеш-индексом и ожиданием свободного места
- `memory_pool.c/h` - Fast object pool allocator (pre-allocated blocks)
- `debug_config.c/h` - Debug logging system (levels, categories, dual output, runtime config)
- `timeout_heap.c/h` - Min-heap for timer management (used by u_async)
@@ -416,7 +446,7 @@ MEMBER_SYNC=27
### Key Components
- **UASYNC:** One per thread. Async event loop (epoll/poll), timers via timeout_heap
- **LL_QUEUE:** Lock-free queue with auto-callback, hash index lookup, threshold waiter
- **LL_QUEUE:** Очередь с auto-callback, хеш-индексом и threshold waiter; не средство обмена между потоками
- **Memory Pool:** Fast allocation for hot-path objects (packets, inflight entries, fragments)
- **ETCP:** TCP-like reliable protocol with encryption, multi-link, load balancing
- **Secure Channel:** AES-CCM + X25519 key exchange, nonce-based encryption, pubkey obfuscation
@@ -432,6 +462,8 @@ MEMBER_SYNC=27
- Очереди забивать нельзя. Добавляй следующий элемент только когда очередь стала пустой.
- Порог задаётся через `queue_set_threshold(q, max_packets, max_bytes)`.
- Используй `queue_waiter_wait` для ожидания освобождения очереди до заданного порога.
- Handle ожидания обнуляется целиком, живёт до callback/отмены и отменяется до освобождения владельца.
Callback может быть синхронным. `queue_entry_free` не освобождает dgram — его освобождают отдельно.
### Чтение из очереди
- Используй `queue_set_callback`: при вызове callback обработай один или несколько элементов, потом вызови `queue_resume_callback`.
@@ -451,7 +483,8 @@ MEMBER_SYNC=27
## Config Rules
- В серверном конфиге только собственные ключи и нет секций `[client]`
- В клиентском конфиге есть собственные ключи и pubkey каждого сервера в секции `[client]`
- Chat-настройки хранятся в секции `[chatserver]` (принимается также `[chat]`). Парсер вызывает `chat_setting_set(key, value)` для каждого ключа — неизвестные ключи вызывают ошибку. Пример:
- Chat-настройки хранятся в `[chatserver]` (также `[chat]`). Парсер обрабатывает общие поля секции,
затем вызывает `chat_setting_set_state(&cfg->global.chat_settings, key, value)`; неизвестные ключи логируются как ошибка. Пример:
```ini
[chatserver]
storage_autoload=1
@@ -474,7 +507,8 @@ MEMBER_SYNC=27
Если видишь спам-логи - подумай как их выборочно отключить чтобы не мешали.
Для отладки добавляй в DEBUG_CATEGORY_DEBUG диагностические сообщения где надо. И включи эту категорию в настройках. Убирай только после проверки (путём запуска) когда все ошибки устранены.
сообщение выводится по или - либо debug_set_level(DEBUG_LEVEL_TRACE) - выводится ВСЁ независимо от настроек по категориям. Тоесть берется max(global level, category lavel)
Явный уровень категории заменяет глобальный. DEBUG_LEVEL_NONE наследует глобальный уровень;
DEBUG_LEVEL_DISABLED полностью отключает категорию, в том числе при глобальном TRACE (см. debug_should_output).
Твой бич - ты постоянно гадаешь и анализируешь код. Что приводит к снежному кобу ошибок и неверных гипотез. 10 раз повторяю - только логи логи логи и никакого гадания. Лоооги!!! правильные логи покажут всё с предельной точностью. Вся суть отладки - информативные логи
И смотри логи. не задавливай их grep-ом. лучше больше. единственное с чем борись - это бесполезные спам логи.
@@ -484,7 +518,8 @@ MEMBER_SYNC=27
Но полезные смотри всегда, и всегда оставляй логи которые выводятся нечасто.
Частые логи - это трафик которые >100 раз повторяются. Но которые мало раз обязательно оставлять и выводить.
можешь в тесте включить логи в нужный интервал чтобы не спамить. debug_set_level(DEBUG_LEVEL_TRACE) и выключить debug_set_level(DEBUG_LEVEL_NONE)
В тесте можно временно поднять нужные категории до TRACE, сохранив прежние уровни и восстановив их после участка.
Одного debug_set_level(DEBUG_LEVEL_NONE) недостаточно для отключения категорий с явно заданным уровнем.
Если видишь проблему и ее решение неочевидно то выстраивай диагностику вокруг неё пока не будет очевидно где и что происходит не так. Это базовое и обязательное требование к отладке.
Подробные логи ты должен выводить не обрезая всегда и анализировать. Если лог большой - выведи в файл и анализируй файл.
Рассуждение должно быть примерно таким:
@@ -535,12 +570,17 @@ MEMBER_SYNC=27
- Обязательно наличие подробной диагностики в функциях кода которые не сильно спамят (не часто вызываются).
- Для каждого отладочного вывода подумай какая из доступной информация будет полезна чтобы можно было наиболее завершенно оценить состояние алгоритма и состояний влияющих на алгоритм.
## Оформление кода
- в .c: перед каждой функцией - краткое но понятное описание что делает функция (кроме совсем простых)
- в .h: в начале - описание модуля - для чего предназначен, как пользоваться, нюансы. Далее структуры с комментариями, далее публичные функции с описаниями (что делает, как пользоваться, какие аргументы, возврат, нюансы работы)
## chatgui (GUI Chat Client)
Chatgui — десктопный GUI-чат на Qt 6 (Qt 5 fallback), отдельный проект внутри репозитория. Не связан с autotools-сборкой utun.
Chatgui (бинарник `vibechat`) — десктопный чат на Qt 6 (Qt 5 fallback), отдельная CMake-сборка общего ядра.
Корневой `build.sh` дополнительно вызывает её, если уже существует `tools/chatgui/build/CMakeCache.txt`.
В chat gui интегрированы библиотеки utun. Чат и библиотеки работают в разных потоках. Поэтому нужно использовать семафоры, сокеты или другие механизмы синхронизации (uasync_post, uasync_memsync, uasync_get_wakeup_fd)
GUI работает в Qt-потоке, ядро и чат — в uasync-потоке. Команды передаются через
`gui_bridge_post_uasync()` / `uasync_post()`, события возвращаются через Qt-сигналы.
База данных sqlite в chatgui: можно писать (update) из потока utun / instance. можно только читать из gui потока.
@@ -548,28 +588,28 @@ Chatgui — десктопный GUI-чат на Qt 6 (Qt 5 fallback), отде
- **Язык:** C++20
- **Фреймворк:** Qt 6 (предпочитаемый) или Qt 5.15+ (Widgets + Network)
- **Сборка:** CMake 3.16+
- **БД:** SQLite3 (WAL mode, встроенный `db/sqlite3.c` amalgamation)
- **БД:** SQLite3 (WAL mode, компилируется общий `lib/sqlite3.c`)
- **Анимации:** rlottie (C API) + zlib (gzip-декомпрессия TGS)
### Сборка
```bash
# Установка зависимостей
sudo apt install librlottie-dev zlib1g-dev qt6-base-dev
sudo apt install librlottie-dev zlib1g-dev qt6-base-dev libssl-dev libx11-dev
# или qtbase5-dev для Qt 5 fallback
# Сборка
cd tools/chatgui && mkdir -p build && cd build
cmake .. && make -j4
./chatgui
./vibechat
```
### Конфиг и логи чатгуи
- **Конфиг:** `tools/chatgui/build/chatgui.cfg` (в `.gitignore`, содержит приватные ключи)
- **Конфиг:** `vibechat.cfg` рядом с бинарником (обычно `tools/chatgui/build/vibechat.cfg`, содержит приватные ключи)
- Секция `[gui]`: `debug_file`, `debug_level`, `debug_categories=cat=level,...`
- Секция `[control]`: `ip`, `port` для подключения etcpmon
- Секция `[ntp]`: синхронизация времени
- **Лог:** `tools/chatgui/build/chatgui.log` (путь задаётся в конфиге `debug_file`)
- **БД чата:** `tools/chatgui/build/chat_data/` (SQLite, путь из `db_path` в конфиге)
- **БД чата:** `chats.db` в каталоге `[gui] db_path` (по умолчанию `chat_data/` рядом с бинарником)
### Структура файлов (src/)
@@ -578,7 +618,7 @@ cmake .. && make -j4
| `main.cpp` | Точка входа, инициализация QApplication |
| `mainwindow.h/cpp` | Главное окно, QSplitter (ChannelList \| MessageList \| AccountList), трей |
| `chatview.h/cpp` | QListView с фоновым изображением, hover-сигнал, drag-to-select |
| `messagelist.h/cpp` | QStandardItemModel, stub-данные, инициализация анимаций, hover→activate |
| `messagelist.h/cpp` | Модель сообщений из БД, обновления доставки/медиа, анимации, состояние просмотра канала |
| `messagedelegate.h/cpp` | QStyledItemDelegate: бабблы, текст, цитаты, реакции, оверлей анимаций |
| `emoji.h/cpp` | EmojiData/EmojiCategory/EmojiType, 4 категории, `g_animatedEmojiMap` |
| `emojipanel.h/cpp` | Эмодзи-пикер: QTabWidget + QGridLayout, EmojiButton с hover-анимацией |
@@ -619,7 +659,8 @@ cmake .. && make -j4
| `debug_ui.h` | Отладочный UI |
### Другие поддиректории
- `db/` — SQLite3 amalgamation (`sqlite3.c/h`), `db_manager.h/cpp` (схема: nodes, node_addresses, channels, msg_<channel>, peers_<channel>, local_identity, accounts, ui_state)
- `db/` — `db_manager.h/cpp`: доступ GUI к данным nodes, channels, msg_<channel>, peers_<channel> и состоянию интерфейса;
копия `db/sqlite3.c/h` не используется текущей CMake-целью, SQLite берётся из `lib/`
- `transport/` — интеграция с uTun: `gui_bridge.h/gui_bridge_impl.cpp` (Qt signal-based обмен), `node_config.h/cpp` (конфиг узла), `config_updater.h/cpp` (правка конфига), `utun_node.h/cpp` (обёртка узла), `miniaudio_impl.c` (single-header miniaudio в отдельной единице трансляции). Chat-логика (`chat_core`, `chat_sync` и т.д.) компилируется напрямую из `src/chat/`, роутинг — из `src/routing_layer/` (см. `libutun/CMakeLists.txt`)
- `resources/` — `bg.jpg`, `chatgui.qrc` (встраивает 5 TGS в бинарник), `animations/*.tgs`
@@ -715,18 +756,19 @@ void lottie_animation_destroy(Lottie_Animation *anim);
---
## Key Documentation Files
- `/doc/etcp_protocol.txt` - ETCP протокол (формат кодограмм, ACK, handshake, keepalive)
- `/doc/etcp_arch.md` - ETCP архитектура
- `/doc/etcp_config.txt` - Конфигурация ETCP
- `/doc/etcp_router_arch.md` - ETCP роутер архитектура
- `/doc/route_p2pconn.txt` - Route peer-to-peer соединения
- `/src/route_bgp.txt` - BGP обмен маршрутами (дизайн-документ)
- `/doc/chat_join_test.md` - ТЗ теста join-протокола (chat_join + дерево приглашений)
- `/doc/chat_join_test_tasks.md` - Список задач по тесту join (текущий статус, пошагово)
- `doc/service_lifecycle.md` — ядро, независимые сервисы, владение ресурсами и остановка
- `doc/node_snapshot.md` — подписанная запись узла, timestamp, SQLite и обновление NCD
- `doc/etcp_protocol.txt` — ETCP: кодограммы, ACK, handshake, keepalive
- `doc/etcp_arch.md` — архитектура ETCP
- `doc/etcp_router_arch.md` — архитектура маршрутизатора
- `src/chat/chat_join.h` — нормативный протокол добавления участника
- `src/routing_layer/topo_group.h` — протокол групповой сессии и критерий READY
- `tests/test_chat_join_e2e.c`, `tests/test_topo_recovery.c`, `tests/test_services.c` — проверки join, recovery и жизненного цикла
- `doc/dm_arch.md`, `doc/chat_call_arch.md` — личные сообщения и звонки
## Список задач по проекту
Список задач — `/doc/tasks.md` (текущая работа, берём по одной задаче сверху).
Список задач — `doc/tasks.md` относительно корня репозитория (текущая работа, берём по одной задаче сверху).
При нахождении попутных багов/fail/флаков тестов добавляем задачу (либо фиксим сразу если баг простой).
Как сделали - помечаем [+] выполнено.
@@ -734,8 +776,9 @@ void lottie_animation_destroy(Lottie_Animation *anim);
**Расположение:** `tools/chatgui-android/`
Android-версия чатгуи — P2P чат на STCP (TCP), UI на Jetpack Compose (Kotlin).
C-ядро: `libutun_lite` (выборочная компиляция нужных .c из `lib/` и `src/`).
Android-версия чатгуи — P2P чат на общем ETCP/STCP-ядре, UI на Jetpack Compose (Kotlin).
C-библиотека `utun_lite` собирает `lib/` и `src/` по `libutun_lite/utun_sources.cmake`;
`instance_lite` запускает ядро и чат в отдельном потоке, без UTUN-сервиса.
Сборка: CMake (headless, Linux) + Gradle/NDK (Android APK).
- 'fw' - собрать и обновить chatgui-android на всех подключенных телефонах (clean + сборка + install)
@@ -743,9 +786,10 @@ C-ядро: `libutun_lite` (выборочная компиляция нужны
**Chat-модули:** все файлы из `src/chat/` (описаны выше в секции «Chat») компилируются в `libutun_lite`.
## Runtime
- Запуск utun от root (для tun): `/home/vnc1/proj/utun3/utun_start.sh`
- Стоп utun: `sudo /home/vnc1/proj/utun3/utun_stop1.sh`
- Логи: `utun.log` (stdout), `utun_err.log` (stderr)
- После autotools-сборки бинарник — `src/utun`. Запуск из корня с TUN: `sudo ./src/utun -f -c utun.cfg`.
- Старый `utun_start.sh` ожидает `./utun` в корне. `utun_stop.sh` делает `killall -9 utun`, а не штатную остановку одного экземпляра.
- `-f` оставляет процесс на переднем плане; остановка — SIGINT/SIGTERM. Пути конфигурации, PID и лога задаются через `-c`, `-p`, `-l`.
- `utun_start.sh` перенаправляет stdout в `utun.log`, stderr в `utun_err.log`.
- Тестовые логи: `tests/logs/`
## Git Conventions
@@ -756,19 +800,19 @@ C-ядро: `libutun_lite` (выборочная компиляция нужны
## Quick Start for New Features
1. Add new source file to `src/Makefile.am` under `utun_SOURCES`
2. Add test file to `tests/Makefile.am` under `check_PROGRAMS`
2. Add test target and its sources/dependencies to `tests/Makefile.am` under `check_PROGRAMS`
3. Use existing patterns from similar modules
5. Run `./check.sh` after changes
4. Run `./check.sh` after changes
5. Commit with descriptive message in appropriate language
## chatgui: отправка сообщений через xdotool
1. **Запуск:** `setsid env DISPLAY=:1.0 QT_ACCESSIBILITY=1 ./chatgui & disown` (иначе SIGTERM убьёт процесс при таймауте bash)
2. **Окно:** `0x4600006 "Chat"` (WM_CLASS "chatgui"), 900×600. Не путать с `0x4800001` (10×10, временное)
3. **Фокус:** клик в область InputBar (x=400, y=570 относительно окна) перед вводом
4. **Отправка:** `xdotool type --window WID "text"` + `xdotool key --window WID Return`
5. **Проверка:** `sqlite3 chats.db "SELECT ... FROM msg_ch_GENERAL"` — сообщения в per-channel таблицах
6. **MCP computer use** не видит chatgui — нужен `qt5-at-spi` bridge (нет в репах, ставить из исходников Qt)
1. **Запуск:** из каталога сборки `setsid env QT_ACCESSIBILITY=1 ./vibechat & disown` с DISPLAY текущей X11-сессии.
2. **Окно:** найти актуальный ID через `xdotool search --onlyvisible --class vibechat`; ID и размеры окна меняются между запусками.
3. **Фокус:** активировать найденное окно и выбрать InputBar; координаты определять по текущему расположению интерфейса.
4. **Отправка:** `xdotool type --window WID "text"` + `xdotool key --window WID Return`, где WID заменён найденным ID.
5. **Проверка:** через `chats.db` из каталога `db_path`; таблицы сообщений называются `msg_<числовой channel_id>`.
6. Доступность UI-автоматизации зависит от текущей X11/Wayland-сессии и настроек Qt accessibility; старые ID окон и DISPLAY не переносить.
---
Эта инструкция имеет приоритет над инструкцией opencode.
+33
View File
@@ -0,0 +1,33 @@
Ниже — подтверждённые по коду проблемы; отдельно указал, что воспроизведено запуском.
[P1] Exit смешивает TCP-потоки разных клиентов. Поиск соединения использует только stream_id, хотя каждый клиент начинает нумерацию заново. После подключения второго клиента с таким же ID данные первого могут уйти в соединение второго; FIN/CLOSE также закрывают чужой поток. Нужен ключ (src_node_id, stream_id) во всех обработчиках, включая проверку повторного CONNECT. src/proxy/tcp_proxy_server.c:342
[P1] Клиент не проверяет отправителя ответов. DATA/FIN/CLOSE принимаются по одному stream_id, без сравнения источника с via_node_id. Обработка CLOSE_ALL читает peer_id, но затем уничтожает вообще все клиентские соединения. Пакеты другого узла, доставленные этому сервису, могут повредить или закрыть существующие потоки. src/proxy/tcp_proxy_client.c:668
[P1, воспроизведено] UDP-ответы в TUN имеют неправильную checksum. Обнуляется только IP-заголовок; байты UDP checksum остаются заполненными аллокатором значением 0xAAAA. Проверка сформированного пакета дала сумму 0xC1E2 вместо 0xFFFF. Такие ответы будут отбрасываться принимающим стеком. Нужно рассчитывать checksum либо явно устанавливать ноль для IPv4. src/proxy/udp_proxy.c:188
[P1, воспроизведено] ICMP с нечётной длиной payload получает неверную checksum. Цикл читает последнее 16-битное слово целиком, захватывая байт за пределами выделенного пользовательского буфера. Ошибка есть и на exit, и при восстановлении ответа клиенту. Для payload длиной 2 проверочная сумма получилась FFFF, длиной 3 — FF10. Отправка (src/proxy/icmp_proxy.c:80), ответ в TUN (src/proxy/icmp_proxy.c:285)
[P1] UDP/ICMP-контексты глобальные, а ядро поддерживает несколько instances. Следующий init() перезаписывает g_udp_ctx/g_icmp_ctx; обработчики старого экземпляра используют новый контекст, а destroy(inst) освобождает его без проверки владельца. Дополнительно UDP различает запрос и ответ только по глобальному is_exit: узел, одновременно работающий клиентом и exit, трактует входящий ответ как новый исходящий запрос. src/proxy/udp_proxy.c:137, инициализация (src/proxy/udp_proxy.c:237), src/proxy/icmp_proxy.c:321
[P1] Нет сквозного ограничения потока при медленном получателе. Принятые ETCP-данные без ограничения добавляются в write_queue сокета или to_lwip. Заполненность этих очередей не останавливает доставку и подтверждение данных маршрутизатором. Медленный destination или локальный клиент приводит к росту памяти; при отказе выделения TCP-данные просто теряются. src/proxy/tcp_proxy_server.c:424, src/proxy/tcp_proxy_client.c:584
[P1] TUN-клиент может уничтожить ещё не отправленные данные при FIN. Если локальный FIN уже получен, но tx_queue остаётся заблокированной, входящий FIN от exit вызывает conn_finish() без проверки этой очереди. Она освобождается вместе с данными. Завершать поток нужно после опустошения очередей обоих направлений. src/proxy/tcp_proxy_client.c:642
[P1] Exit может отправить CLOSE раньше остатка ответа. В on_fin_cb() проверка tc->fin_local имеет приоритет над pend_r. Если клиент уже закрыл свою половину, а ответ destination остался в tx_buf из-за backpressure, сервер отправляет CLOSE и освобождает остаток. Ветка on_flushed_cb() также не проверяет ожидающие отправки данные ответа. src/proxy/tcp_proxy_server.c:108
[P1] HTTP CONNECT зависит от границ TCP-чтений. Парсер запускает туннель после первой \r\n, не дожидаясь \r\n\r\n. Если заголовки приходят следующим чтением, они пересылаются destination как содержимое туннеля — например, перед TLS ClientHello. Если вместе с заголовками уже пришли данные туннеля, они уничтожаются обнулением buf_len. src/proxy/socks_proxy.c:195
[P1] HTTP POST во время DNS может потерять весь накопленный запрос. dns_pending накапливается до 65535 байт, затем отправляется одним DATA. Exit принимает максимум 8192 байта в одном сообщении и отбрасывает превышение. При переполнении самого dns_pending очередные данные также теряются без прекращения потока. Нужны ограниченная очередь и отправка частями. src/proxy/socks_proxy.c:339, накопление (src/proxy/socks_proxy.c:430), ограничение exit (src/proxy/tcp_proxy_server.c:438)
[P2] FIN теряется в двух штатных сценариях. SOCKS/HTTP при непустой очереди ответа вызывает tcp_conn_set_flushed(tc, NULL) — продолжение, которое должно переслать FIN, отсутствует. Exit молча игнорирует FIN, пришедший до завершения TCP connect. Протоколы, ожидающие EOF перед ответом, могут зависнуть. src/proxy/socks_proxy.c:538, src/proxy/tcp_proxy_server.c:492
[P2] SOCKS5-парсер некорректно обрабатывает запросы. После greeting/CONNECT сбрасывается весь буфер, включая следующие байты; всегда выбирается метод NO AUTH, даже если клиент его не предлагал. IPv6 вместо отказа превращается в IPv4 из неправильного смещения buf + 12. Доменный CONNECT длиной менее 10 байт бесконечно ожидает продолжения. src/proxy/socks_proxy.c:132
[P2] SOCKS/HTTP сообщают об успешном соединении до подключения exit. Ответ SOCKS success или HTTP 200 формируется даже до send_connect(). Подтверждения успешного TCP connect от exit в протоколе нет. При отказе подключения клиент сначала получает успех, затем закрытие вместо корректной ошибки. src/proxy/socks_proxy.c:316
[P2] После RST TUN-соединение остаётся в списке. tcp_proxy_client_err_cb() обнуляет pcb и выставляет error, но не освобождает pc. Очистка предусмотрена в poll callback уже уничтоженного PCB, который больше не вызовется. src/proxy/tcp_proxy_client.c:446
[P2] Неправильно разбираются IP options и фрагменты в TUN. UDP/ICMP используют фиксированные смещения от 20-байтового IPv4-заголовка, игнорируя IHL и fragment offset. Пакет с options или последующий IP-фрагмент превращается в запрос с неверными портами/данными. src/proxy/tcp_proxy_client.c:158
[P2] ICMP-ответы разных клиентов могут перепутаться. Exit сохраняет исходные echo_id/echo_seq и ищет ответ только по этой паре, без уникального преобразования ID и проверки адреса отправителя. Совпадающие ping-запросы разных клиентов получают чужие ответы. src/proxy/icmp_proxy.c:56
+2
View File
@@ -1,3 +1,5 @@
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](src/routing_layer/etcp_router_doc.md).
# План: эпоха (reset_id) для ETCP Router
Аналог фикса `reset_id` из ETCP (см. `61c572c`), применённый к сервисному слою
+2
View File
@@ -170,6 +170,8 @@ if [ "$USE_ASAN" -eq 1 ]; then
AF="-fsanitize=address -fno-omit-frame-pointer -g -Og"
AL="-fsanitize=address"
(cd lib && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$AF" "LDFLAGS+=$AL" $JOBS $MAKE_ARGS 2>&1) || { echo "lib build failed"; exit 1; }
# libopus собирается без ASAN (собственный CFLAGS в Makefile, .a несовместим с PIE)
(cd lib/libopus && make clean $JOBS $MAKE_ARGS 2>&1 && make $JOBS $MAKE_ARGS 2>&1) || { echo "libopus build failed"; exit 1; }
(cd src && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$AF" "LDFLAGS+=$AL" $JOBS $MAKE_ARGS 2>&1) || { echo "src build failed"; exit 1; }
BUILD_SUCCESS=1
elif [ "$USE_HARDENED" -eq 1 ]; then
+1 -1
View File
@@ -70,7 +70,7 @@ echo "Building utun.exe..."
cd src
EXE_NEEDS_REBUILD=0
CORE_SOURCES="utun.c utun_instance.c config_parser.c config_updater.c routing_layer/route_lib.c topo_bgp.c routing_layer/routing.c tun_if.c tun_route.c transport_layer/etcp.c transport_layer/etcp_connections.c transport_layer/etcp_loadbalancer.c transport_layer/secure_channel.c transport_layer/crc32.c transport_layer/pkt_normalizer.c transport_layer/etcp_api.c tun_windows.c"
CORE_SOURCES="utun.c utun_instance.c config_parser.c config_updater.c routing_layer/route_lib.c topo_bgp.c routing_layer/routing.c tun_if.c tun_route.c transport_layer/etcp.c transport_layer/etcp_session.c transport_layer/etcp_connections.c transport_layer/etcp_loadbalancer.c transport_layer/secure_channel.c transport_layer/crc32.c transport_layer/pkt_normalizer.c transport_layer/etcp_api.c tun_windows.c"
for src in $CORE_SOURCES; do
if [ -f "$src" ]; then
+6
View File
@@ -20,9 +20,15 @@ rm -f "$TMP"
echo ""
if [ "$(id -u)" -eq 0 ]; then
make -C tests check-proxy
make -C tests check-burst
make -C tests check-load
elif command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
echo "=== tcp_proxy_full integration test (sudo) ==="
sudo -n make -C tests check-proxy
echo "=== tcp_proxy_full burst test (sudo) ==="
sudo -n make -C tests check-burst
echo "=== tcp_proxy_full load test (sudo) ==="
sudo -n make -C tests check-load
else
echo "[SKIP] tcp_proxy_full integration test (requires passwordless sudo)"
fi
+14 -1
View File
@@ -95,6 +95,19 @@ AC_MSG_RESULT([$os_freebsd])
AM_CONDITIONAL([OS_FREEBSD], [test "x$os_freebsd" = "xyes"])
# ==================== DETECT LINUX ====================
AC_MSG_CHECKING([for Linux])
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
#ifndef __linux__
#error "Not Linux"
#endif
]], [[ return 0; ]])],
[os_linux=yes],
[os_linux=no])
AC_MSG_RESULT([$os_linux])
AM_CONDITIONAL([OS_LINUX], [test "x$os_linux" = "xyes"])
# Set TUN libs based on platform
if test "x$os_windows" = "xyes"; then
TUN_LIBS="-liphlpapi -lws2_32 -ladvapi32 -lbcrypt -ldbghelp"
@@ -197,4 +210,4 @@ Configuration summary:
Prefix: ${prefix}
Compiler: ${CC}
CFLAGS: ${CFLAGS}
"
"
+1 -1
View File
@@ -197,7 +197,7 @@ SRC_SOURCES=(
routing_layer/route_lib.c routing_layer/route6_lib.c topo_bgp.c routing_layer/route_ping.c
routing_layer/topo_node.c topo_node_lmdb.c routing_layer/route_connectivity.c routing_layer/conn_mgr.c
routing_layer/routing.c tun_if.c tun_route.c tun_linux.c tun_freebsd.c tun_windows.c
transport_layer/etcp.c transport_layer/etcp_connections.c transport_layer/etcp_bbr.c transport_layer/etcp_loadbalancer.c
transport_layer/etcp.c transport_layer/etcp_session.c transport_layer/etcp_connections.c transport_layer/etcp_bbr.c transport_layer/etcp_loadbalancer.c
transport_layer/etcp_debug.c transport_layer/etcp_dump.c transport_layer/secure_channel.c transport_layer/crc32.c
transport_layer/stcp_link.c transport_layer/stcp.c transport_layer/stcp_server.c transport_layer/stcp_client.c
transport_layer/pkt_normalizer.c transport_layer/packet_dump.c transport_layer/etcp_api.c
+18 -1
View File
@@ -63,10 +63,19 @@ Callee: IDLE → INCOMING(INVITE) → RINGING(sent RINGING) → ACCEPTING(accept
NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL`.
Таймауты:
- ring-timeout 45с (нет ответа → CANCEL);
- **кодограмма INVITE 300мс** — вызывающий шлёт её весь период вызова
(`OUTGOING` + `RINGING`) как «сердцебиение»: покрывает потерю INVITE/RINGING и
держит маршрут живым;
- **watchdog 2с у вызываемого** — нет кодограммы 2с → вызов считается сорванным
(отмена потерялась), сессия закрывается локально (`CALL_REASON_CANCEL`);
- ring-timeout 45с (вызывающий, нет ответа → CANCEL);
- connect: `conn_mgr` `CONN_EVENT_TIMEOUT` → fail;
- **NO_TRAFFIC 20с** — нет входящих медиа-кадров → авто HANGUP.
Завершающие кодограммы (HANGUP/CANCEL/DECLINE) шлются без ретрансмиссий — потеря
компенсируется watchdog'ом 2с у вызываемого. Запоздалые кодограммы вызова при
идущем разговоре (`CONNECTING`/`ACTIVE`) игнорируются.
Онлайн-проверка: `topo_node_find_by_id(group, peer) == NULL` → «не в сети» (event).
Соединение: на ACCEPT обе стороны `conn_mgr_open(inst, group_id, peer, cb, ...)`;
@@ -127,6 +136,14 @@ API: `ajb_create / ajb_push / ajb_pull / ajb_stats / ajb_destroy`.
## 8. Тоны и диагностика
- **Исходящий звонок — две фазы с разными гудками** (у вызывающего):
- фаза 1 «пытаемся связаться» (`OUTGOING`: INVITE отправлен/ретранслируется, маршрут
устанавливается) — гудок «соединение» (короткие бипы);
- фаза 2 «звоним» (`RINGING`: получен RINGING, у пира идёт вызов) — длинные гудки (ringback).
Гудки генерируются платформенно-нативно: desktop — цикличные MP3 через `SoundManager`
(`call_connecting`/`call_ringing`), Android — `ToneGenerator`
(`TONE_SUP_DIAL`/`TONE_SUP_RINGTONE`). Переключение — по `CHAT_EVT_CALL_RINGING`,
остановка — по `CALL_ACCEPTED`/`CALL_ENDED`/`CALL_ERROR`/hangup.
- **Сталл >500мс** (нет входящих медиа) → в `call_pull_pcm` микшируется
`CALL_TONE_GLITCH`: `пи-пи (2×150мс, gap 100мс) … тишина до 1.0с … пи(200мс)
пауза(200мс) пи(200мс)`, 1000 Гц, цикл пока длится сталл.
+178
View File
@@ -0,0 +1,178 @@
# Разбор полного прогона 2026-09-27
Исходный результат `./check.sh` вне песочницы: 92 passed, 5 failed, 1 skipped (98 тестов).
Песочница запрещает сетевые сокеты; её результаты не использовались для оценки протокола.
Промежуточный прогон: 96 passed, 1 failed, 1 skipped; оставался `test_chat_join_e2e`.
После KEY_REGISTER_ACK и исправления невыровненных чтений повторный полный
`make check -j4`: **97 passed, 0 failed, 1 skipped (98)**.
Лог: `/tmp/utun-ack-final-check.log`. Исходное падение отдельного root-теста и
последующая доработка описаны ниже. После исправления autosockets общий набор дал
97 PASS / 1 SKIP, а `check-proxy`, `check-burst`, `check-load` прошли:
`/tmp/root-fix-full-check.log`.
`test_etcp_lifecycle`, `test_etcp_link_stress`, `test_conn_mgr_phases` дополнительно
прошли ASan/UBSan/LeakSanitizer. Инструментированы тесты и изменённые транспортные/
маршрутные модули, остальные объекты библиотек взяты из обычной сборки.
Логи: `/tmp/ncd-late-asan.log`, `/tmp/link-role-asan.log`, `/tmp/cm-phases-asan.log`.
## Исправленные причины
- `test_conn_mgr_phases`: после TIMEOUT фазы DIRECT NCD навсегда подавлял UP,
хотя conn_mgr сохранял handle для REVERSE. В логе физический ETCP уже UP,
но conn_mgr продолжал DIRECT_REQ до своего таймаута. TIMEOUT теперь обозначает
истечение срока первой попытки, CLOSED — окончательное завершение. Удерживаемый
handle получает поздний UP. Отдельный lifecycle-регрессионный тест воспроизводит
TIMEOUT → INIT/UP (одно уведомление) → DOWN → UP; до исправления он падал.
- `test_etcp_link_stress`: ожидал reset всей сессии при коллизии дополнительного
линка. Проверка заменена на сохранение эпох, отсутствие reset и дублей линков,
доставку и ACK сообщений через новый линк. Прежний тест забирал сырые фрагменты
вместо normalizer и мог считать служебные пакеты полезным трафиком. Теперь он
использует отдельный ETCP service и проверяет каждый байт и порядок сообщений.
Эта проверка выявила ошибку реализации: при принятии встречного INIT сохранённый
исходящий линк не менял `is_server`. Роль обновляется при отправке INIT_RESPONSE.
- `test_bgp_route_exchange`: после разрыва B–C recovery успешно строил C–A,
нарушая предположение теста о фиксированной цепочке. Теперь A и C принимают
только ключ B: все прежние проверки withdraw/restore сохранены, обход запрещён
условиями тестовой сети. Автоматический обход отдельно проверяет `test_topo_recovery`.
- `test_tcp_io`: адрес 192.0.2.1:81 в окружении устанавливал TCP-соединение,
поэтому connect timeout закономерно не срабатывал. Теперь используется локальный
listener с заполненной accept-очередью, проверяется ETIMEDOUT. Перед уничтожением
uasync тест завершает отложенное освобождение TCP и проверяет баланс таймеров.
Диагностика тестов включается `UTUN_TEST_DEBUG=1`; обычный запуск не включает DEBUG.
Остаточные DATA после удаления соединения могут быть undecryptable и сами по себе
не означают ошибку транспорта.
## Исправлено: гонка регистрации приглашения
`test_chat_join_e2e`, happy path A != C, падал из-за отсутствия подтверждения KEY_REGISTER.
Раньше `chat_invite_build_link()` вызывал `chat_join_register_key()` и сразу возвращал ссылку.
Успех `etcp_route_send()` означает постановку в очередь. Он не подтверждает обработку
ключа посредником C. Параллельное подключение J–C может завершиться раньше доставки A–C.
Доказательство: `/tmp/join-e2e-debug.log`, один ключ `90c4b081833450a0`:
- 01:39:02.437851 — A пишет `join-key registered`, фактически ключ только поставлен в отправку.
- 01:39:02.446527 — C отвергает JOIN_INFO_REQ: ключ ещё неизвестен.
- 01:39:02.449724 — C сохраняет этот ключ, но J уже получил терминальный ERROR.
Теперь C отвечает KEY_REGISTER_ACK только после сохранения ключа. A сопоставляет
channel/target/key и выдаёт ссылку после успешного ACK и локального сохранения ключа.
Отрицательный ACK завершает запрос ошибкой. Чужие, неподписанные, незашифрованные,
повторные и запоздалые подтверждения не могут завершить другой запрос.
Контракт асинхронных API `chat_join_register_key` / `chat_invite_build_link`:
NULL означает ошибку запуска без callback; принятый запрос даёт ровно один callback.
Обычное завершение происходит после возврата API, включая self. Cancel/destroy
завершают запрос синхронно с CANCELLED. Handle недействителен после callback.
Для удалённого узла дедлайн 5 секунд; доставку и повтор потерянных пакетов обеспечивает
надёжный router. Повторная регистрация того же ключа тем же инвайтером обновляет TTL;
замена инвайтера для существующего ключа запрещена. Отмена может оставить ключ на C
до истечения TTL, но A не авторизует по нему вход, поскольку локально такой ключ
сохраняется только при успешном завершении.
GUI получает request_id в событии и игнорирует устаревшие результаты. Новый запрос
отменяет предыдущее ожидание GUI. Headless связывает ответ с JSON id и отменяет все
ожидания отключившегося клиента. Ни один из путей не запускает вложенный event loop.
Дополнительные исправления:
- Устранён double-free при ошибке `etcp_route_send`: пакет уже освобождён роутером.
- Headless прекращает разбор пакета после `quit`; последующая команда не создаёт
запрос на уже закрытом клиенте. При destroy отменяются cleanup_timer и запросы,
удаляются клиентские сокеты и освобождается контекст.
- UBSan обнаружил невыровненное чтение group_id из `d + 1` в chat_sync. Оно и три
аналогичных диагностических чтения заменены на memcpy.
- Тест chat_join теперь останавливает вручную запущенный member_sync;
проверяется баланс выделения/освобождения таймеров.
Проверки:
- `test_chat_join`: 31/31, включая задержанный/отсутствующий ACK, таймаут,
неверные channel/peer/key/flags/размер/status, дубликат, отказ хранения из-за
конфликта инвайтера, отмену, shutdown, параллельные ключи и ошибку отправки.
- `test_chat_join_e2e`: 3/3 (A≠C, A=C, неверный ключ). C принудительно откладывает
регистрацию на 200 мс; проверяется отсутствие ссылки до обработки регистрации.
Дополнительно проверяются подавление старого GUI-запроса и реальный headless
control socket: параллельные ответы, валидность ссылок, quit и команда после него.
- ASan/UBSan: оба теста проходят. LeakSanitizer и баланс таймеров проверены в
test_chat_join; E2E использует fork/_exit, поэтому сам по себе не проверяет утечки
при завершении дочерних процессов. Инструментированы изменённые chat-модули и
transport/routing, остальные объекты взяты из обычной библиотеки.
Логи: `/tmp/utun-ack-join-asan.log`, `/tmp/utun-ack-e2e-asan.log`.
## Отдельный root-тест auto_socket_dynamic
Запуск в `sudo unshare --net` с поднятым loopback закончился TIMEOUT через 30 секунд.
Лог: `/tmp/utun-ack-root-checks.log`. Это не PASS основного набора: в нём тест пропущен.
В конфиге теста включён auto_sockets, но не отключён стандартный фильтр
`auto_socket_skip_no_default_route=1`. Dummy-интерфейсы теста не имеют default route,
а `auto_socket.c:reconcile_iface()` исключает такие интерфейсы. Лог показывает
`no links created`, затем TIMEOUT. Исправленный тест создаёт собственный network
namespace, поднимает loopback и добавляет default route только клиентским dummy.
Фильтр остаётся включённым; отдельно проверяется исключение серверного dummy,
иначе его сокет скрывал бы полный обрыв клиентской сети.
Кроме того, тест сохраняет handle общего дедлайна, traffic_send и traffic_monitor
в одну переменную timeout_handle; фазовые таймеры вообще не сохраняются. При выходе
не отменялись все таймеры; зафиксирован остаток 9 таймеров (665/656). В обработчиках
трафика освобождался entry без queue_dgram_free. Теперь у каждого таймера свой slot,
waiters отменяются до teardown, payload освобождается, deferred cleanup завершается
до проверки баланса таймеров, сокетов и `u_get_allocated_count()`.
Новый генератор шлёт 1024-байтовые пакеты независимо в обе стороны, по одному через
threshold waiter, с бюджетом 64 пакета на tick. Проверяются содержимое, строгий seq,
ограниченность TX/RX очередей, реальное срабатывание backpressure, остановка
генераторов при полном обрыве и полное опустошение после восстановления. NCD handle
удерживает логическую сессию: смена интерфейса не является разрешением терять её данные.
Остатки после действительно удалённого соединения — отдельный допустимый случай.
Прогон нагрузки обнаружил ошибки, скрытые прежними проверками:
- `tcp_socket_add` не сохранял `netif_index`: монитор принимал interface-bound сокет
за сокет default route и менял его адрес при событиях другого интерфейса.
- Удаление TCP модели искало её по имени уже удалённого интерфейса и порту из БД.
Теперь запись интерфейса хранит имя и прямую ссылку на модель; один путь закрывает
listener, линки и модель. Портовые записи удаляются с сохранённым именем.
- `tcp_socket_remove` не закрывал линки, созданные вне NCD. Теперь закрывает все
связанные линки до освобождения модели; root-тест создаёт такой линк отдельно.
- Поиск UDP модели не исключал TCP. Добавлена проверка типа сокета.
- При selective ACK sender мог выдавать новые seq за границей `MAX_INFLIGHT_SIZE`
относительно cumulative ACK. Теперь генерация seq останавливается на границе,
cumulative ACK возобновляет очередь даже при отсутствии уже selectively-acked записей.
Тесты проверяют блокировку, возобновление и uint32 wrap. ACK не может подтвердить
ещё не назначенный seq; декодирование старшего байта seq/ACK не делает signed shift.
- Timestamp Q16 умножался как signed int и переполнялся на циклических/отрицательных
смещениях. Перевод выполняется в uint32. Jitter раньше всегда получал нулевую разницу:
предыдущий RTT читался после перезаписи; теперь берётся до неё, EWMA использует int64.
- UBSan выявил невыровненные canary/указатели в `mem.c` и `memory_pool.c`. Метаданные
читаются/пишутся через memcpy; покрыты размеры блоков 1..17 и повторное использование пула.
- Повторный общий прогон поймал `test_etcp_connect`: PID-порты пересекались с занятым
UDP-портом (`EADDRINUSE`). Теперь сервер bind-ится на порт 0, клиент получает фактический
порт через getsockname; собственного алгоритма поиска свободного порта нет.
Root ASan/UBSan/LSan: PASS, 153335 / 175912 пакетов, reinit=0, все очереди пусты,
баланс памяти/таймеров/сокетов совпал (`/tmp/root-fix-asan.log`). Инструментированы
тест, ETCP/session/router/NCD/autosocket/STCP/normalizer, очереди, event loop и аллокаторы;
прочие объекты архивов обычные. Unit-логи: `/tmp/root-session-asan.log`,
`/tmp/root-memory-asan.log`. LSan запускается вне sandbox: под ptrace он не работает.
Финальная чистая обычная сборка: `./check.sh` — **97 PASS / 0 FAIL / 1 SKIP**;
proxy, burst и load — PASS (`/tmp/root-fix-final-check.log`). Отдельный root-тест —
PASS, 188069 / 191552 пакета, все очереди пусты, reinit=0, баланс ресурсов совпал
(`/tmp/root-test-final.log`). После добавления drain отложенной очистки
`test_etcp_connect` повторно прошёл без остаточных таймеров (`/tmp/root-connect-final.log`).
Подробные socket/connection INFO включаются только через `UTUN_TEST_DEBUG=1`.
`ENETUNREACH` непосредственно при принудительном удалении IP/интерфейса ожидаем до
обработки netlink; тест требует последующей доставки всех данных. Ошибки выхода
за receive window недопустимы и после исправления sender gate не наблюдаются.
## Попутная проблема диагностики
`lib/debug_config.c:debug_parse_config()` требует `:`, хотя документация описывает `=`.
Кроме того, функция всё ещё вычисляет индекс через битовую маску, хотя категории
теперь являются индексами. Исправление в этот набор не включено; тесты используют
`debug_set_category_level()` напрямую.
+37
View File
@@ -41,6 +41,12 @@ inflight - две очереди (ll_queue):
- send_hist[8] - история через какие линки отправлялся (индекс линка)
- inflight_bytes/inflight_packets per-link отслеживается в ETCP_LINK
Размер буфера inflight лимитируем как сумму по всем активным линкам optimal_inflight.
Отдельно ограничиваем размах sequence относительно кумулятивного ACK: новый seq выдаётся
только если (uint32_t)(next_tx_id - rx_ack_till) <= MAX_INFLIGHT_SIZE (16384).
Selective ACK освобождает отдельную inflight-запись, но не продвигает эту границу:
приёмник пока не может освободить пакеты за дыркой. Кумулятивный ACK возобновляет
input_queue даже если подтверждённые записи уже удалены по selective ACK.
ACK ограничивается последним назначенным seq (next_tx_id - 1); сравнения учитывают uint32 wrap.
optimal_inflight расчитывается из RTT и bandwidth линка.
bandwidth по каждому линку адаптивно подстраиваем:
@@ -187,3 +193,34 @@ flag_up в заголовке устанавливается в значение
- сервер как получает init проверяет session_id:
- если session_id совпадает — шлёт подтверждение без сброса (0x05)
- если session_id изменился (клиент перезапустился) — шлёт подтверждение со сбросом (0x03)
**** Connection session confirmation (2026-09) ****
Physical UDP INIT / STCP handshake establishes an authenticated link. A stream reset
uses the same control protocol on every initialized UDP or TCP link, bypassing the
normalizer and reliable DATA/ACK queues. A physical reset does not generate DOWN/UP.
Encrypted DATA/ACK plaintext after timestamp(2) + flag_up(1):
09 | sender_epoch:u64be | receiver_epoch:u64be | existing ACK/DATA sections
The extra 17 bytes participate in the MTU budget. Unwrapped DATA/ACK and frames with
an epoch mismatch are discarded before stream state or link liveness is updated.
Control frames (all 25 bytes after timestamp/flag):
0a HELLO | sender_epoch | receiver_epoch | 0
0b CHALLENGE | sender_epoch | receiver_epoch | random_cookie:u64be
0c CONFIRM | sender_epoch | receiver_epoch | echoed_cookie:u64be
Each connection owns one pending peer epoch, fresh challenge, and retry timer.
HELLO and a changed epoch advertised by INIT only request a challenge. Only a
CONFIRM for the current local epoch and pending random challenge commits a peer
epoch. Changing the peer epoch resets the stream but preserves our own epoch.
A local fatal reset generates a new local epoch and blocks DATA until confirmation.
Both sides may reset concurrently. Loss of any control frame is recovered by a
500 ms retry on all initialized links. After 10 s an unconfirmed local reset closes
the connection; an unconfirmed candidate alone is discarded without closing a
working session. Close cancels the timer before destroying links.
TCP may initially receive peer epoch zero because the server creates ETCP_CONN
only after STCP handshake. It completes the same session confirmation before INIT/UP.
REINIT notifies stream consumers without tearing down physical ownership. The
router retains end-to-end inflight; BGP repeats JOIN_GROUP and TABLE_REQUEST because
its previous control messages may have been discarded by the stream reset.
+2
View File
@@ -1,3 +1,5 @@
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](../src/routing_layer/etcp_router_doc.md).
# ETCP Router Architecture
## Структуры данных
+60
View File
@@ -0,0 +1,60 @@
# Подписанная запись узла
`TOPO_NODE.timestamp` — версия всей подписанной записи, а не отдельного адреса.
Узел-владелец назначает Unix microseconds, строго больше предыдущей сохранённой
версии. Имя, тип, активность, адреса, метаданные сокетов, REALITY и подпись
заменяются вместе. Timestamp входит в подпись. Групповые пути и подсети
остаются в `TOPO_GROUP_NODE` и не являются частью общей записи узла.
Получатель проверяет подпись до изменения реестра или маршрутов. Более свежая
запись заменяет старую целиком, включая пустые списки адресов. Старые записи
игнорируются; разные подписанные записи с одинаковым timestamp — конфликт.
Повторное BGP-объявление может обновить путь, но не содержимое общей записи.
8-битный `ver` сохраняется как диагностический счётчик; свежесть определяется
только timestamp.
Таблица `node_snapshots` хранит полную сериализованную запись без группового
окружения. `node_addresses` остаётся проекцией для поиска и отображения, а не
источником для восстановления подписанной записи. При наличии сохранённого
snapshot загрузка использует его. Локальная публикация сохраняется до рассылки;
ошибка сохранения не разрешает публикацию новой версии.
Timestamp 0 обозначает bootstrap-сведения без подписи (конфиг, приглашение,
начальные адреса). Они могут помочь установить первое соединение, но не
заменяют принятую подписанную запись. По BGP принимаются подписанные записи
с ненулевым timestamp. Формат NODEINFO изменён без обратной совместимости.
После принятия BGP-записи `node_conn_direct_update_node` применяет адреса к
существующему NCD-соединению. Отсутствующее соединение этот вызов не создаёт.
Линки дополняются как при UP, так и до установления соединения; повторная
доставка не создаёт дубликаты UDP/TCP. Работающие линки не закрываются только
из-за смены списка адресов. NCD удерживает ссылку на общую запись, пока
обслуживает соединение, и использует её при восстановлении.
`node_conn_direct_open_node` сверяет переданную запись с реестром/БД. Более
свежая подписанная запись принимается целиком, старая уступает уже известной.
Bootstrap-адреса не перекрывают подписанную запись даже при DOWN.
Конфигурационные подключения UTUN используют один адаптер
`init_client_connections` для старта и reload. Все адреса, TCP и REALITY
передаются NCD; адаптер не создаёт ETCP-линки. Новый набор handles открывается
до освобождения старого; при ошибке старые handles сохраняются. Конфигурация
удерживает один handle на клиента, независимо от количества его адресов.
Включение конфигурационного пира в UTUN-группу выполняет callback NCD UP.
Транспортный UP и наличие callback старого `etcp_connect` больше не включают
соединение в UTUN-группу автоматически. Входящий JOIN остаётся явным запросом
участия. При пересоздании UTUN-группы учитываются её конфигурационные handles.
Удаление клиента отправляет `LEAVE_GROUP` и освобождает удержание UTUN-группы
с обеих сторон. Другие владельцы NCD сохраняют транспорт. Поздние NODEINFO и
WITHDRAW от отсоединённого UTUN-пира игнорируются, чтобы не вернуть его маршруты.
LEAVE ждёт backpressure через queue waiter и временно удерживает собственный
NCD handle. Закрытие транспорта отменяет ожидание; новое участие в группе
делает ожидающий LEAVE устаревшим, и он не отправляется.
Причина удаления пира передаётся явно: только потеря транспорта запускает
recovery каскадных маршрутов. Локальный/удалённый LEAVE и удаление битой
member-записи не должны восстанавливать намеренно закрытое участие.
REALITY использует полное SNI до 255 символов; размер поля в записи узла
согласован с транспортом. Если локального TCP listen-сокета нет, NCD может
создать исходящий TCP-линк с выбором локального адреса операционной системой.
+61
View File
@@ -0,0 +1,61 @@
# Proxy: потоки TCP и датаграммы
Клиентские входы — TUN (lwIP), SOCKS5 CONNECT и HTTP proxy/CONNECT. Exit открывает
обычный TCP-сокет к назначению. TCP-поток на exit определяется парой `(peer_node_id,
stream_id)`. Клиент принимает ответы и уведомления restart только от настроенного
exit в UTUN-группе. UDP/ICMP-контексты принадлежат конкретному `UTUN_INSTANCE`.
## TCP
Общие определения находятся в `src/proxy/proxy_protocol.h`. Передаваемый заголовок:
`svc_id:1, command:1, stream_id:4`; поля маршрута добавляет ETCP-router.
- CONNECT (1): IPv4:4 и порт:2 в сетевом порядке.
- CONNECTED (2): exit подтвердил успешный TCP connect. Только после этого клиент
сообщает SOCKS success / HTTP 200 и начинает передавать накопленные данные.
- DATA (3): от 1 до 4096 байт.
- CLOSE (4), ERROR (5): прекращение потока.
- FIN (6): конец одного направления; все предшествующие DATA должны быть переданы.
- WINDOW (7): uint32_t, число дополнительно разрешённых байт.
У каждого направления начальное окно 65536 байт. DATA уменьшает окно отправителя
и получателя. Получатель возвращает кредит после записи в локальный TCP-сокет
либо принятия данных ограниченным send-buffer lwIP. Очередь ETCP сама по себе
не является подтверждением потребления данных конечным TCP.
FIN ждёт освобождения исходящей очереди, включая данные, остановленные окном
или backpressure роутера. Закрытие одного направления не прекращает другое.
Окончательное освобождение происходит после обоих FIN и передачи оставшихся данных.
Непереданные управляющие сообщения повторяются таймером. Ошибки и переходы
состояния диагностируются категорией `proxy`; состояние сокетов — `socket`.
Парсеры сохраняют остаток после greeting/request/HTTP headers. HTTP CONNECT ждёт
полного блока заголовков. Во время DNS/connect чтение ограничено очередью tcp_io;
накопленные HTTP headers/body передаются частями размером DATA.
## UDP и ICMP
REQUEST и REPLY различаются явно. Ответ допускается только от настроенного exit.
UDP-ответ восстанавливается с нулевой UDP checksum (допустимо для IPv4), IPv4 checksum
пересчитывается. ICMP checksum учитывает нечётный последний байт без чтения за буфером.
Exit заменяет ICMP id/sequence уникальной среди ожидающих запросов парой и сопоставляет
ответ также с IP назначения. Исходные id/sequence восстанавливаются для клиента.
Это исключает смешивание ping-запросов разных клиентов с одинаковыми исходными ID.
## Проверки и ограничения
`test_proxy_packets` проверяет IPv4/UDP/ICMP и payload длиной 0..1501.
`test_tcp_io_flush` проверяет уведомление после последнего синхронного send.
`test_proxy_regressions` использует реальные локальные сокеты и управляемую доставку
ETCP: совместные/раздельные handshake, ранние данные, отложенный CONNECTED, HTTP 502,
большой POST, одинаковые stream ID разных пиров, FIN при закрытом окне, RST и IPv4 options.
На Linux полный `./check.sh`: 109 passed, 0 failed, 1 skipped; дополнительно прошли
настоящий TUN, full-duplex burst и нагрузка 8 соединений / 128 МиБ.
Windows/FreeBSD в этом прогоне не проверялись.
SOCKS IPv6 явно отклоняется кодом address type not supported. IPv4-фрагменты
на входе proxy TUN отклоняются с диагностикой; сборка фрагментов не реализована.
Формат TCP дополнен CONNECTED/WINDOW, UDP-команды разделены: клиент и exit необходимо
обновлять вместе; совместимость со старым proxy-протоколом не предусмотрена.
+59
View File
@@ -0,0 +1,59 @@
# Жизненный цикл ядра и сервисов
Все операции выполняются в потоке единственного `UASYNC` экземпляра, вне callbacks
останавливаемых модулей. GUI передаёт команды в этот поток.
```c
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config);
if (!inst || utun_core_start(inst) < 0) /* обработать ошибку */;
if (chat_service_start(inst) < 0) /* обработать ошибку */;
if (utun_service_start(inst) < 0) /* обработать ошибку */;
chat_service_stop(inst); /* UTUN продолжает работать */
chat_service_start(inst); /* загружает сохранённые каналы */
utun_service_stop(inst); /* чат продолжает работать */
utun_instance_destroy(inst);
```
`utun_instance_init()` — сокращение для запуска ядра, UTUN и чата, если чат включён
в конфигурации. Desktop GUI и Android запускают ядро и чат явно, без UTUN.
Повторный start работающего сервиса и повторный stop допустимы. Ошибка старта
сервиса откатывает созданные им ресурсы. `utun_instance_stop()` останавливает цикл
приложения; освобождение ядра выполняет `utun_instance_destroy()`.
| Владелец | Ресурсы |
|---|---|
| Ядро | Идентичность узла, реестр узлов и групп, SQLite, транспортные сокеты, NCD, маршрутизатор, общая репликация, время, мониторинг |
| UTUN | Группа UTUN, запросы соединений из конфигурации, DATA binding, TUN, системные маршруты, NAT transport |
| Чат | CHAT-группы, запросы подключения и join, экземпляры синхронизации каналов, chat API, медиа, личные сообщения, звонки и headless API |
| Группа | Сессии пиров, её маршруты и логические каналы, планировщик подключения и recovery |
| Сессия пира группы | NCD handle с момента CONNECTING, согласованные эпохи, состояние обмена таблицей, очередь отправки |
SQLite открывается ядром один раз: `db_path/chats.db`, либо `:memory:`, если путь
не задан. Остановка чата сохраняет каналы и сообщения в этой БД. В варианте
`:memory:` данные сохраняются только до уничтожения ядра.
Общий транспорт живёт, пока есть его владельцы. Закрытие запроса одной группы
не разрывает соединение, нужное другой группе. При остановке группы отменяются
её планировщики, запросы, таймеры и ожидания очередей; отправляется LEAVE,
закрываются логические каналы маршрутизатора и удаляется её ожидающий транзит
на общих соединениях. Транзит других групп сохраняется. Отложенный LEAVE может временно
удерживать транспорт самостоятельно, не обращаясь к уже удалённой группе.
Остановка чата сначала прекращает внешние команды и фоновые задания, затем
закрывает медиа и сервисы, синхронизацию и группы, после чего освобождает chat core.
`media_async_destroy()` дожидается завершения native workers и вызывает каждый
ожидающий callback с `MEDIA_ASYNC_CANCELLED`, пока его контекст ещё существует.
Результаты отменённых задач не записываются в БД. Это синхронное ожидание: длительное
транскодирование или распознавание речи может задержать stop; принудительного
прерывания этих библиотек нет.
Статус транспорта UP подтверждает восстановление физического пути. READY сессии
группы означает согласованный JOIN/ACCEPT и завершённый обмен её таблицей; именно
его ждёт recovery. REINIT меняет эпохи обмена, сохраняя рабочие маршруты. Эпохи
защищают от сообщений старой сессии и не задают срок жизни маршрута.
Проверки: `test_services` проверяет независимый stop/start, общий NCD в CONNECTING,
сохранение идентичности/сокетов/БД, повторную загрузку каналов, отмену workers,
заблокированного медиа-потока, транзита и ожиданий заменённой очереди. `test_chat_join_e2e`
проверяет join и распространение membership между узлами без UTUN-сервиса.
+102 -1
View File
@@ -3,6 +3,29 @@
Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ.
## Текущая задача
[+] **test_auto_socket_dynamic: root-регрессия и нагрузка** — изолированный namespace,
корректные маршруты, двусторонний поток через backpressure, строгие seq/payload,
пределы TX/RX, остановка при обрыве и drain после восстановления, баланс ресурсов.
Исправлены привязка TCP к интерфейсу, удаление TCP моделей/линков/портов из БД,
sender receive window при selective ACK, timestamp overflow и нулевой jitter.
ASan/UBSan/LSan: PASS; дополнительно исправлены невыровненные метки аллокаторов
и PID-порты в test_etcp_connect. [Разбор и логи](etcp_full_suite_findings.md).
[ ] **debug_parse_config: формат и индексы категорий** — parser ожидает `:`, документация
описывает `=`; преобразование битовой маски осталось после перехода enum на индексы.
[Разбор](etcp_full_suite_findings.md).
[+] **Q8/Android не подключается к локальному чатгуи (LAN)** — сделано. Причина: тип сокета
в Android-конфиге был захардкожен `type=public`, из-за чего `sock_match` ставил
`has_priv=0` и не создавал линки к приватным (LAN) адресам пиров (`create_links … → 0 links`).
Исправлено: (1) `ip_is_public()` вынесен в `lib/platform_compat` (единый классификатор
IPv4 с корректным ntohl); (2) в `auto_socket_reconcile_config` (Android-режим) тип сокета
выставляется по адресу (public→PUBLIC, приватный/link-local→NAT), конфиг `type` игнорируется;
(3) `sock_collect_views` больше не смотрит на `type` из конфига: `has_priv` — по классификации
локального адреса, `has_inet=1` (исходящие считаем доступными), `is_eim` — по NAT-детекции
(hairpin). `is_local_subnet` упрощён до `!ip_is_public()`. Тест `test_sock_match` обновлён
(+ `ip_is_public` кейсы). `check.sh`: 86 passed / 1 failed / 1 skipped.
[ ] **DM: прямой p2p чат с любым пользователем группы** — отдельная DM-подсистема.
ТЗ и архитектура: `/doc/dm_arch.md`. Статус: реализовано (dm_core/dm_mailbox/dm_crypto),
тесты `test_dm` (крипто) и `test_dm_e2e` (интеграция). Остались задачи ниже.
@@ -12,7 +35,73 @@
(локальный буфер отправки при переполнении роутера) + адаптивный джиттер-буфер
(SoundTouch, ускорение до 1.6x) + тоны (глитч/завершение) + watchdog 20с.
Android: переключение источника (трубка/громкая/гарнитура).
Архитектура: `/doc/chat_call_arch.md`. Статус: спроектировано, ждёт реализации.
Архитектура: `/doc/chat_call_arch.md`.
[+] **Сигналинг + релей медиа** — сделано. `call_proto.h` (svc `ETCP_RT_ID_CALL
0x36`, subcmd INVITE/RINGING/ACCEPT/DECLINE/BUSY/CANCEL/HANGUP/MEDIA),
`call.c` (машина состояний, реестр сессий, conn_mgr, ring-timeout 45с,
watchdog no-traffic 20с, потеря соединения → end, события
CHAT_EVT_CALL_* 34–40), `DEBUG_CATEGORY_CALL 32`. `test_call` (2 инстанса,
happy path + decline + media-релей). Приём медиа — через `call_set_media_recv_cb`.
[+] **Headless API** — сделано. Команды control-сокета `call_start/accept/decline/
hangup` + события `call_incoming/ringing/accepted/ended/declined` (JSON);
аудио-сокет `call/call_headless.c` (TCP, бинарный фрейм
`[type:1][call_id:8][len:2][data]`, HELLO/FRAME), конфиг `call_audio_bind`.
Разрыв conn_mgr — отложенно 500мс (`CALL_TEARDOWN_DELAY_TB`) после ENDED,
чтобы HANGUP успел долететь; защита от stale-handle через conn-status.
Тесты: `test_call` (сигналинг+медиа), `test_call_headless` (полный e2e через
control+audio сокеты).
[+] **Аудио-контур (time-stretch в сервисном слое)** — сделано. `src/call/audio_jitter.cpp`
(SoundTouch, C API `ajb_*`: адаптивный джиттер-буфер ≤1000мс, target 60мс,
tempo 1.0→1.6x — догон live без потери данных при сетевой паузе),
`src/call/call_tones.c` (глитч при сталле >500мс + тон завершения),
`src/call/call_audio.c` (Opus 48kHz/20мс + jitter + тоны + pull API финального PCM).
`call.c`: `call_audio_ops` (on_media c seq/ts, get_stats) + событие
`CHAT_EVT_CALL_STATS` раз в 1с [rtt_ms, buffer_ms, tempo_x100].
Desktop `call_audio_engine.cpp` — только miniaudio I/O (получает финальный PCM).
Тест `test_audio_jitter` (ёмкость/догон/стационар). Демон остаётся C-only.
[+] **Android** — сделано. JNI (`utun_bridge_call_*`/`nativeCall*`), AudioRecord/Track
(CallAudioEngine), роутинг источника (CallAudioRouter: EARPIECE/SPEAKER/HEADSET,
BT SCO), CallScreen + IncomingCallDialog + RingtonePlayer + IncomingCallManager,
`libutun_lite/call_audio.c` (собственный аудио-контур Opus 48kHz/20мс).
[+] **Единый голосовой стек (voice stack) + SoundTouch на Android** — сделано.
Унификация аудио-контура звонка: `libutun_voice` (статическая C++-либа с
C-интерфейсом `call_audio.h` = SoundTouch + `voice_jitter.cpp` + `call_audio.c`
+ `call_tones.c`), собирается через `src/call/voice_sources.cmake` и линкуется
desktop/Android/headless-CLI. Джиттер-буфер держит КОДИРОВАННЫЕ (Opus) кадры,
декодирует только на pull (непосредственно перед воспроизведением) — память ~48x
меньше и uasync-поток разгружен. Android переведён с push-FIFO (Kotlin ArrayDeque)
на pull-модель (`nativeCallAudioPull`), своего джиттера больше нет → убрана
асимметрия задержки (desktop↔Android). Headless-аудио-сокет отдаёт готовый PCM
(декод Opus→PCM, без тактирования; клиент сам задаёт темп). Диагностика:
`CALL_STATS` расширен [dropped, underruns] (раз в 1с), счётчики headless.
Удалены `audio_jitter.cpp` и `libutun_lite/call_audio.c`. Тест `test_voice_jitter`
(ёмкость/догон/стационар). Android — `ANDROID_STL=c++_static`.
[ ] **DM-звонки** (fallback `group_id==0`) — не сделано: `call_is_online`/`call_start`
требуют реальную группу (`topo_groups_find`), для DM (group_id==0) маршрута нет.
## Звонок: две фазы исходящего вызова (индикация + гудки)
[+] **Две фазы вызова** — сделано. Исходящий звонок разбит на 2 фазы с индикацией
в окне вызова и разными гудками:
- фаза 1 «Пытаемся связаться…» (`OUTGOING` — INVITE/маршрут) — гудок «соединение»;
- фаза 2 «Звоним…» (`RINGING` — у пира идёт вызов) — длинные гудки (ringback).
Desktop: `CallWindow::Ringing` + `SoundManager` loop `call_connecting`/`call_ringing`
(MP3 в qrc), переключение по `onCallRinging`, остановка по accepted/ended/error;
добавлена обработка `GUI_EVT_CALL_ERROR` (40) — раньше при «пир офлайн» окно/гудок
зависали навсегда. Android: `CallRingback` (MediaPlayer loop из res/raw),
подписи `OUTGOING`/`RINGING` в CallScreen + ActiveCallBanner.
Звуковые события добавлены в SoundSettingsPage (call_connecting/call_ringing).
[+] **Надёжность отмены вызова (кодограмма + watchdog)** — сделано. Отмена вызова
(HANGUP/CANCEL) шлётся один раз, без ретрансмита, и при флапе маршрута (RST/epoch,
прямой TCP к мобиле не поднимается — только релей) терялась: вызываемый висел в
RINGING вечно и отвечал BUSY на все новые INVITE. Исправлено моделью «сердцебиения»:
- вызывающий шлёт кодограмму INVITE каждые 300мс весь период вызова (OUTGOING+RINGING);
- у вызываемого watchdog 2с — нет кодограммы → вызов сорван (`CALL_REASON_CANCEL`);
- завершающие кодограммы без ретрансмиссий (потерю покрывает watchdog);
- запоздалые кодограммы вызова при разговоре (CONNECTING/ACTIVE) игнорируются.
`CALL_INVITE_RETRY_TB=3000`, `CALL_RING_DEAD_TB=20000`, `CALL_NO_ROUTE_MAX_STRIKES=20`.
## Рефакторинг: per-instance chat/dm (однопоточный test_dm_e2e)
@@ -132,6 +221,18 @@
`router_no_route`), не влияет на результат — стоит разобрать отдельно.
## Открытые флаки
[+] **test_chat_join_e2e** — сделано. Причина: ложный критерий успеха джойнера J.
`wait_group_started()` проверял `senders_list` непуст в предположении «senders_list
непуст ⟺ J получил JOIN_READY». После коммита b483335c `tgi_to_channel_cb`
(topo_group_invite.c) при `NCD_EVENT_UP` стал вызывать `topo_group_new_conn` →
conn попадает в `senders_list` сразу после установки TCP/UDP-соединения, ДО
завершения хендшейка (JOIN_INFO_REQ→RESP→REQUEST→READY). J выходил «OK» раньше,
чем A получал JOIN_REQUEST → A не подписывал J → `wait_signed` на A/C падал
(`get_sign_rc=-1`). Фикс: критерий J — событие `CHAT_EVT_CONNECT_RESULT(result=0)`
(JOIN_READY), ловится через `chat_event_set_handler`; `wait_group_started` удалён.
Это также убрало гонку «C вышел раньше, чем J синкает свой рекорд» (C выходит сразу
после `wait_signed`, не дожидаясь merkle-синка J). 5 прогонов стабильно 3/3.
[ ] **test_etcp_reconnect** — флаки под параллельной нагрузкой `make check -j4`:
phase 4 (reconnect после server restart) таймаутит (`sent=480 recv=0`), при одиночном
запуске стабильно `TEST PASSED`. Наш код (chat_join/chat_sync) его не трогает.
+1 -1
View File
@@ -251,7 +251,7 @@ static void adns_rearm(struct adns_query* q) {
}
q->cur_fd = -1;
if (fd >= 0) {
q->socket_id = uasync_add_socket_t(q->ua, (socket_t)fd, adns_read_cb, adns_write_cb, NULL, q);
q->socket_id = uasync_add_socket_t(q->ua, (socket_t)fd, adns_read_cb, adns_write_cb, NULL, "dns_query", q);
if (q->socket_id) q->cur_fd = fd;
else DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: uasync_add_socket_t(%d) failed", fd);
}
+26 -7
View File
@@ -16,6 +16,7 @@ struct audio_compressor {
int lookback_blocks;
int lookahead_blocks;
float rise_factor_per_block;
float release_factor_per_block;
float max_gain;
float gain_smoothed;
@@ -38,6 +39,7 @@ struct audio_compressor {
size_t output_size;
size_t output_cap;
uint32_t clip_count;
int dbg_counter;
};
@@ -79,13 +81,16 @@ void audio_compressor_configure(struct audio_compressor* ac, const audio_compres
ac->lookback_blocks = ac->cfg.lookback_ms / ac->cfg.block_duration_ms;
ac->lookahead_blocks = ac->cfg.lookahead_ms / ac->cfg.block_duration_ms;
ac->rise_factor_per_block = powf(ac->cfg.rise_rate_per_sec, (float)ac->cfg.block_duration_ms / 1000.0f);
if (ac->cfg.release_rate_per_sec <= 0.0f)
ac->cfg.release_rate_per_sec = ac->cfg.rise_rate_per_sec * 5.0f;
ac->release_factor_per_block = powf(ac->cfg.release_rate_per_sec, (float)ac->cfg.block_duration_ms / 1000.0f);
ac->max_gain = powf(10.0f, ac->cfg.max_gain_db / 20.0f);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
"audio_compressor_configure: rate=%d ch=%d block=%dms lookback=%dms lookahead=%dms maxGain=%.0fdB riseRate=%.1f/sec target=%.0fdBFS",
"audio_compressor_configure: rate=%d ch=%d block=%dms lookback=%dms lookahead=%dms maxGain=%.0fdB riseRate=%.1f/sec releaseRate=%.1f/sec target=%.0fdBFS",
ac->cfg.sample_rate, ac->cfg.channels, ac->cfg.block_duration_ms,
ac->cfg.lookback_ms, ac->cfg.lookahead_ms, ac->cfg.max_gain_db,
ac->cfg.rise_rate_per_sec, 20.0f * log10f(ac->cfg.target_level));
ac->cfg.rise_rate_per_sec, ac->cfg.release_rate_per_sec, 20.0f * log10f(ac->cfg.target_level));
}
void audio_compressor_reset(struct audio_compressor* ac) {
@@ -97,6 +102,7 @@ void audio_compressor_reset(struct audio_compressor* ac) {
ac->accum_count = 0;
ac->output_size = 0;
ac->gain_smoothed = 1.0f;
ac->clip_count = 0;
ac->dbg_counter = 0;
}
@@ -227,25 +233,29 @@ static void process_pending_block(struct audio_compressor* ac) {
float G_prev = ac->gain_smoothed;
if (G_raw > ac->gain_smoothed) {
/* сигнал стал тише — плавно поднимаем gain (атака) */
float max_rise = ac->gain_smoothed * ac->rise_factor_per_block;
ac->gain_smoothed = G_raw < max_rise ? G_raw : max_rise;
} else {
ac->gain_smoothed = G_raw;
/* сигнал стал громче — плавно опускаем gain (release), без мгновенной ступеньки
* (мгновенный дроп давал клик на границе 20мс-блока). */
float min_fall = ac->gain_smoothed / ac->release_factor_per_block;
ac->gain_smoothed = G_raw > min_fall ? G_raw : min_fall;
}
if (ac->gain_smoothed > ac->max_gain) ac->gain_smoothed = ac->max_gain;
float gain_db = 20.0f * log10f(ac->gain_smoothed > 0.0001f ? ac->gain_smoothed : 0.0001f);
float prev_db = 20.0f * log10f(G_prev > 0.0001f ? G_prev : 0.0001f);
if (ac->dbg_counter % 25 == 0 || fabsf(gain_db - prev_db) > 3.0f) {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor: block %zu level=%.4f envelope=%.4f gainRaw=%.1fdB gain=%.1fdB",
idx, ac->block_levels[idx], envelope, 20.0f * log10f(G_raw), gain_db);
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor: block %zu level=%.4f envelope=%.4f gainRaw=%.1fdB gain=%.1fdB clip=%u",
idx, ac->block_levels[idx], envelope, 20.0f * log10f(G_raw), gain_db, (unsigned)ac->clip_count);
}
ac->dbg_counter++;
for (size_t i = 0; i < ac->pending[0].count; i++) {
float v = (float)ac->pending[0].samples[i] * ac->gain_smoothed;
if (v > 32767.0f) v = 32767.0f;
if (v < -32768.0f) v = -32768.0f;
if (v > 32767.0f) { v = 32767.0f; ac->clip_count++; }
if (v < -32768.0f) { v = -32768.0f; ac->clip_count++; }
ac->pending[0].samples[i] = (int16_t)(int)v;
}
append_output(ac, ac->pending[0].samples, ac->pending[0].count);
@@ -290,3 +300,12 @@ const int16_t* audio_compressor_output(const struct audio_compressor* ac) {
size_t audio_compressor_output_size(const struct audio_compressor* ac) {
return ac ? ac->output_size : 0;
}
void audio_compressor_clear_output(struct audio_compressor* ac) {
if (!ac) return;
ac->output_size = 0;
}
uint32_t audio_compressor_clip_count(const struct audio_compressor* ac) {
return ac ? ac->clip_count : 0;
}
+8
View File
@@ -18,6 +18,7 @@ typedef struct {
int lookahead_ms;
float max_gain_db;
float rise_rate_per_sec;
float release_rate_per_sec; /* скорость плавного снижения gain (0 = авто: rise*5) */
float target_level;
} audio_compressor_config_t;
@@ -36,6 +37,13 @@ void audio_compressor_flush(struct audio_compressor* ac);
const int16_t* audio_compressor_output(const struct audio_compressor* ac);
size_t audio_compressor_output_size(const struct audio_compressor* ac);
/* Сбросить накопленный выход (output_size=0) после слива — для потокового режима.
* НЕ трогает gain_smoothed (адаптивная громкость сохраняется). */
void audio_compressor_clear_output(struct audio_compressor* ac);
/* Число сэмплов, обрезанных по потолку int16 (клиппинг) с момента create/reset. */
uint32_t audio_compressor_clip_count(const struct audio_compressor* ac);
#ifdef __cplusplus
}
#endif
+2
View File
@@ -132,6 +132,8 @@ static const struct {
{"video", DEBUG_CATEGORY_VIDEO},
{"reality", DEBUG_CATEGORY_REALITY},
{"dm", DEBUG_CATEGORY_DM},
{"call", DEBUG_CATEGORY_CALL},
{"radio", DEBUG_CATEGORY_RADIO},
{"all", DEBUG_CATEGORY_ALL},
{NULL, DEBUG_CATEGORY_NONE}
};
+3 -1
View File
@@ -70,7 +70,9 @@ typedef int debug_category_t;
#define DEBUG_CATEGORY_VIDEO 29 // Video module (probe/transcode)
#define DEBUG_CATEGORY_REALITY 30 // REALITY TLS camouflage
#define DEBUG_CATEGORY_DM 31 // DM — direct p2p chat between two users
#define DEBUG_CATEGORY_COUNT 32 // Total number of categories
#define DEBUG_CATEGORY_CALL 32 // Call — P2P audio call (signaling + media)
#define DEBUG_CATEGORY_RADIO 33 // Radio — PTT walkie-talkie multicast (voice)
#define DEBUG_CATEGORY_COUNT 34 // Total number of categories
#define DEBUG_CATEGORY_ALL (-1) // special value for all categories
/* Debug configuration structure */
+27 -10
View File
@@ -1,4 +1,6 @@
#include "json_flat.h"
#include "mem.h"
#include "debug_config.h"
#include <string.h>
#define SKIP_WS(p, end) while ((p) < (end) && (*(p) == ' ' || *(p) == '\t' || *(p) == '\n' || *(p) == '\r')) (p)++
@@ -19,6 +21,8 @@ static int json_read_string(const char** pp, const char* end, char* out, size_t
case 'n': ch = '\n'; break;
case 'r': ch = '\r'; break;
case 't': ch = '\t'; break;
case 'b': ch = '\b'; break;
case 'f': ch = '\f'; break;
default: return -1;
}
}
@@ -33,20 +37,22 @@ static int json_read_string(const char** pp, const char* end, char* out, size_t
return 0;
}
static int json_parse_pair(const char** pp, const char* end,
static int json_parse_pair(const char** pp, const char* end, char* scratch, size_t capacity,
json_flat_cb cb, void* arg) {
const char* p = *pp;
SKIP_WS(p, end); CHECK(p, end);
if (*p == '}') { *pp = p; return 0; }
char key[128], val[256];
if (json_read_string(&p, end, key, sizeof(key)) != 0) return -1;
char* key = scratch;
if (json_read_string(&p, end, key, capacity) != 0) return -1;
size_t key_size = strlen(key) + 1;
char* val = scratch + key_size;
SKIP_WS(p, end); CHECK(p, end);
if (*p != ':') return -1; p++;
SKIP_WS(p, end); CHECK(p, end);
if (json_read_string(&p, end, val, sizeof(val)) != 0) return -1;
if (json_read_string(&p, end, val, capacity - key_size) != 0) return -1;
if (cb && cb(key, val, arg) != 0) { *pp = p; return 1; }
if (cb) { int rc = cb(key, val, arg); if (rc != 0) { *pp = p; return rc < 0 ? -1 : 1; } }
SKIP_WS(p, end); CHECK(p, end);
if (*p == ',') p++;
@@ -61,13 +67,23 @@ int json_flat_parse(const char* json, json_flat_cb cb, void* arg) {
SKIP_WS(p, end); CHECK(p, end);
if (*p != '{') return -1; p++;
/* Decoded key + value + two NULs fit within the encoded object size. */
size_t capacity = strlen(json) + 1;
if (capacity > UINT32_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "json_flat: input too large"); return -1; }
char local[384];
char* scratch = capacity <= sizeof(local) ? local : u_malloc(capacity);
if (!scratch) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "json_flat: allocation failed bytes=%zu", capacity); return -1; }
int result = 0;
for (;;) {
int rc = json_parse_pair(&p, end, cb, arg);
if (rc < 0) return -1;
if (rc > 0) return 0; /* callback stopped */
SKIP_WS(p, end); CHECK(p, end);
if (*p == '}') return 0;
int rc = json_parse_pair(&p, end, scratch, capacity, cb, arg);
if (rc < 0) { result = -1; break; }
if (rc > 0) break;
SKIP_WS(p, end);
if (p >= end) { result = -1; break; }
if (*p == '}') break;
}
if (scratch != local) u_free(scratch);
return result;
}
struct get_ctx { const char* key; char* out; size_t sz; int found; };
@@ -81,6 +97,7 @@ static int get_cb(const char* k, const char* v, void* arg) {
}
int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz) {
if (!key || (value_out && !value_sz)) return -1;
struct get_ctx g = { key, value_out, value_sz, 0 };
if (json_flat_parse(json, get_cb, &g) != 0) return -1;
return g.found ? 0 : -1;
+2 -1
View File
@@ -2,7 +2,7 @@
* @file json_flat.h
* @brief Парсер плоского JSON без вложенности: {"key":"value",...}
*
* Только строковые значения. Без malloc, без зависимостей.
* Только строковые значения. Callback получает полные строки без обрезания.
*/
#ifndef JSON_FLAT_H
@@ -15,6 +15,7 @@ extern "C" {
#endif
typedef int (*json_flat_cb)(const char* key, const char* value, void* arg);
/* Callback: 0 = continue, >0 = stop successfully, <0 = error. */
int json_flat_parse(const char* json, json_flat_cb cb, void* arg);
int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz);
+29 -14
View File
@@ -194,6 +194,19 @@ void queue_resume_callback(struct ll_queue* q) {
}
}
// Общий запуск коллбэка автозабора: immediate (default) или deferred через uasync_call_soon.
// Deferred-режим очищает стек производителя перед обработкой первого элемента.
static void queue_trigger_callback(struct ll_queue* q) {
if (!q->head || q->callback_suspended || !q->callback) return;
if (q->callback_defer) {
if (!q->resume_timeout_id)
q->resume_timeout_id = uasync_call_soon(q->ua, q, queue_resume_timeout_cb);
} else {
q->callback(q, q->callback_arg);
}
}
void queue_set_size_limit(struct ll_queue* q, int lim) {
if (!q) return;
q->size_limit = lim;
@@ -384,9 +397,7 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
// НЕ вызываем add_to_hash
if (q->count == 1 && !q->callback_suspended && q->callback) {
q->callback(q, q->callback_arg);
}
if (q->count == 1) queue_trigger_callback(q);
#ifdef QUEUE_DEBUG
queue_check_consistency(q);
@@ -433,9 +444,7 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) {
add_to_hash(q, entry);
if (q->count == 1 && !q->callback_suspended && q->callback) {
q->callback(q, q->callback_arg);
}
if (q->count == 1) queue_trigger_callback(q);
#ifdef QUEUE_DEBUG
queue_check_consistency(q);
@@ -471,9 +480,7 @@ int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry) {
entry->int_len = entry->len;
q->total_bytes += entry->int_len;
if (q->count == 1 && !q->callback_suspended && q->callback) {
q->callback(q, q->callback_arg);
}
if (q->count == 1) queue_trigger_callback(q);
#ifdef QUEUE_DEBUG
queue_check_consistency(q);
@@ -520,9 +527,7 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry)
add_to_hash(q, entry);
if (q->count == 1 && !q->callback_suspended && q->callback) {
q->callback(q, q->callback_arg);
}
if (q->count == 1) queue_trigger_callback(q);
#ifdef QUEUE_DEBUG
queue_check_consistency(q);
@@ -647,6 +652,11 @@ void queue_set_waiter_defer(struct ll_queue* q, int enable) {
q->waiter_defer = enable;
}
void queue_set_callback_defer(struct ll_queue* q, int enable) {
if (!q) return;
q->callback_defer = enable;
}
void queue_set_empty_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg) {
if (!q) return;
void* old = q->empty_call_soon_id;
@@ -694,9 +704,14 @@ 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) {
if (!q || !h || !h->internal) return;
if (!q || !h) return;
/* Отложенный вызов (waiter_defer) может висеть и при internal==NULL:
* queue_waiter_wait по ветке «очередь свободна» планирует call_soon_id,
* не регистрируя waiter. Ранний выход по !internal оставлял бы висячий
* callback с arg=handle, который после teardown разыменовывал бы freed-память. */
if (h->call_soon_id) { uasync_call_soon_cancel(q->ua, h->call_soon_id); h->call_soon_id = NULL; }
if (!h->internal) return;
struct queue_waiter* waiter = h->internal;
struct queue_waiter* prev = NULL;
struct queue_waiter* curr = q->waiter_head;
+12
View File
@@ -149,6 +149,7 @@ struct ll_queue {
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;
@@ -241,6 +242,17 @@ 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 очередь
+1
View File
@@ -195,6 +195,7 @@ queue_set_empty_callback(q, on_empty, my_data);
| `queue_set_callback(q, cbk_fn, arg)` | Установить callback автозабора. Вызывается при наличии элементов. |
| `queue_data_get(q)` | Извлечь элемент из головы. Приостанавливает callback до `queue_resume_callback()`. |
| `queue_resume_callback(q)` | **Обязательно** вызвать после обработки элемента. Планирует отложенный вызов callback через uasync_call_soon. |
| `queue_set_callback_defer(q, enable)` | `enable=1` — первый запуск callback из `put` откладывается через uasync_call_soon (очистка стека). По умолчанию `0` (синхронно). Повторные запуски всегда deferred. |
### Запись
+17 -4
View File
@@ -18,6 +18,9 @@
#define PREV_OFFSET LOCATION_MAX
#define NEXT_OFFSET (LOCATION_MAX + POINTER_SIZE)
// Порог детекта аномально больших аллокаций (гигантские mmap-резервации, напр. VSZ ~4G)
#define LARGE_ALLOC_LOG_THRESHOLD (8u * 1024u * 1024u) // 8 МБ
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
#include <execinfo.h>
#include <pthread.h>
@@ -106,9 +109,10 @@ void u_check(void* ptr, const char* text, const char* location) {
error = true;
}
}
uint32_t* post_canary = (uint32_t*)(user + size);
if (*post_canary != CANARY) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: overflow canary corrupted at %p (expected 0x%X, got 0x%X)", location, text, ptr, CANARY, *post_canary);
uint32_t post_canary;
memcpy(&post_canary, user + size, sizeof(post_canary));
if (post_canary != CANARY) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: overflow canary corrupted at %p (expected 0x%X, got 0x%X)", location, text, ptr, CANARY, post_canary);
error = true;
}
if (error) {
@@ -121,6 +125,10 @@ void u_check(void* ptr, const char* text, const char* location) {
}
void* u_malloc_impl(uint32_t size, const char* location) {
if (size >= LARGE_ALLOC_LOG_THRESHOLD) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "LARGE ALLOC: %u bytes (%.1f MB) at %s",
size, (double)size / (1024.0 * 1024.0), location);
}
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
uint8_t* base = (uint8_t*)malloc(total);
if (!base && size > 0) return NULL;
@@ -134,7 +142,8 @@ void* u_malloc_impl(uint32_t size, const char* location) {
uint8_t* prefix_start = base + METADATA_SIZE;
*(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE) = CANARY;
*(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE + 4) = size;
*(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE + 8 + size) = CANARY;
const uint32_t post_canary = CANARY; /* размер пользователя не обязан быть кратен выравниванию uint32_t */
memcpy(prefix_start + BOUNDARY_CHECK_SIZE + 8 + size, &post_canary, sizeof(post_canary));
// Add to list (thread-safe)
LOCK();
void** prev_ptr = (void**)(base + PREV_OFFSET);
@@ -151,6 +160,10 @@ void* u_malloc_impl(uint32_t size, const char* location) {
}
void* u_calloc_impl(uint32_t nmemb, uint32_t size, const char* location) {
if (nmemb != 0 && size > UINT32_MAX / nmemb) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "CALLOC OVERFLOW: %u * %u (wraps uint32) at %s", nmemb, size, location);
return NULL;
}
uint32_t total_size = nmemb * size;
void* ptr = u_malloc_impl(total_size, location);
if (ptr) memset(ptr, 0, total_size);
+12 -10
View File
@@ -14,34 +14,36 @@
static void pool_init_tags(struct memory_pool* pool, void* obj, const char* location)
{
uint32_t* canary = (uint32_t*)((uint8_t*)obj + POOL_CANARY_OFF);
const char** loc = (const char**)((uint8_t*)obj + POOL_LOC_OFF);
const uint32_t canary = POOL_CANARY_VAL;
uint8_t* counter = (uint8_t*)obj + POOL_COUNTER_OFF;
*canary = POOL_CANARY_VAL;
*loc = location;
/* Метаданные следуют за payload произвольного размера и могут быть невыровненными. */
memcpy((uint8_t*)obj + POOL_CANARY_OFF, &canary, sizeof(canary));
memcpy((uint8_t*)obj + POOL_LOC_OFF, &location, sizeof(location));
*counter = 1;
}
static void pool_check_and_clear_tags(struct memory_pool* pool, void* obj, const char* location)
{
uint32_t* canary = (uint32_t*)((uint8_t*)obj + POOL_CANARY_OFF);
const char** loc = (const char**)((uint8_t*)obj + POOL_LOC_OFF);
uint32_t canary;
const char* loc;
memcpy(&canary, (uint8_t*)obj + POOL_CANARY_OFF, sizeof(canary));
memcpy(&loc, (uint8_t*)obj + POOL_LOC_OFF, sizeof(loc));
uint8_t* counter = (uint8_t*)obj + POOL_COUNTER_OFF;
if (*canary != POOL_CANARY_VAL) {
if (canary != POOL_CANARY_VAL) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free BUFFER OVERFLOW pool=%p name=%s sz=%zu allocs=%zu reuse=%zu free=%d",
pool, pool->name ? pool->name : "?", pool->object_size, pool->allocations, pool->reuse_count, pool->free_count);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, " obj=%p alloc=%s free=%s canary=0x%08x expected=0x%08x",
obj, *loc ? *loc : "(null)", location, *canary, POOL_CANARY_VAL);
obj, loc ? loc : "(null)", location, canary, POOL_CANARY_VAL);
if (pool->object_size)
log_dump(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_SYS, " user_data", obj, pool->object_size);
volatile int _halt = 1;
while (_halt) {}
}
if (*counter == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free DOUBLE FREE pool=%p name=%s sz=%zu allocs=%zu reuse=%zu alloc=%s free=%s",
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free DOUBLE FREE pool=%p name=%s sz=%zu allocs=%zu reuse=%zu obj=%p alloc=%s free=%s",
pool, pool->name ? pool->name : "?", pool->object_size, pool->allocations, pool->reuse_count,
*loc ? *loc : "(null)", location);
obj, loc ? loc : "(null)", location);
volatile int _halt = 1;
while (_halt) {}
}
+5
View File
@@ -146,6 +146,11 @@ int opus_codec_encoder_signal_set(opus_codec_encoder_t* enc, int signal_type) {
return opus_encoder_ctl(enc->opus_enc, OPUS_SET_SIGNAL(signal_type));
}
int opus_codec_encoder_application_set(opus_codec_encoder_t* enc, int application) {
if (!enc || !enc->opus_enc) return -1;
return opus_encoder_ctl(enc->opus_enc, OPUS_SET_APPLICATION(application));
}
const char* opus_codec_strerror(int err) {
switch (err) {
case OPUS_OK: return "OK";
+5
View File
@@ -10,6 +10,10 @@ extern "C" {
typedef struct opus_codec_encoder opus_codec_encoder_t;
typedef struct opus_codec_decoder opus_codec_decoder_t;
/* Режим кодирования Opus: голос (VOIP, default) или музыка (AUDIO). */
#define OPUS_CODEC_APP_VOIP 2048 /* OPUS_APPLICATION_VOIP */
#define OPUS_CODEC_APP_AUDIO 2049 /* OPUS_APPLICATION_AUDIO */
opus_codec_encoder_t* opus_codec_encoder_create(int sample_rate, int channels);
void opus_codec_encoder_destroy(opus_codec_encoder_t* enc);
int opus_codec_encode(opus_codec_encoder_t* enc, const int16_t* pcm, int frame_samples,
@@ -23,6 +27,7 @@ int opus_codec_decode(opus_codec_decoder_t* dec, const uint8_t* data, int len,
int opus_codec_encoder_bitrate_set(opus_codec_encoder_t* enc, int bitrate_bps);
int opus_codec_encoder_complexity_set(opus_codec_encoder_t* enc, int complexity);
int opus_codec_encoder_signal_set(opus_codec_encoder_t* enc, int signal_type);
int opus_codec_encoder_application_set(opus_codec_encoder_t* enc, int application);
const char* opus_codec_strerror(int err);
+19
View File
@@ -39,6 +39,25 @@
#endif
#endif
/*
* Классифицирует IPv4-адрес (network byte order):
* 1 = глобальный/публичный (маршрутизируемый в интернете),
* 0 = приватный, CGNAT, loopback, link-local, multicast или зарезервированный.
*/
int ip_is_public(uint32_t addr_be) {
uint32_t a = ntohl(addr_be);
if ((a & 0xFF000000u) == 0x0A000000u) return 0; /* 10.0.0.0/8 */
if ((a & 0xFFF00000u) == 0xAC100000u) return 0; /* 172.16.0.0/12 */
if ((a & 0xFFFF0000u) == 0xC0A80000u) return 0; /* 192.168.0.0/16 */
if ((a & 0xFF000000u) == 0x7F000000u) return 0; /* 127.0.0.0/8 */
if ((a & 0xFFFF0000u) == 0xA9FE0000u) return 0; /* 169.254.0.0/16 */
if ((a & 0xFF000000u) == 0x00000000u) return 0; /* 0.0.0.0/8 */
if ((a & 0xF0000000u) == 0xE0000000u) return 0; /* 224.0.0.0/4 */
if ((a & 0xF0000000u) == 0xF0000000u) return 0; /* 240.0.0.0/4 */
if ((a & 0xFFC00000u) == 0x64400000u) return 0; /* 100.64.0.0/10 (CGNAT) */
return 1;
}
/*
* Generate cryptographically secure random bytes
* Returns 0 on success, -1 on error
+5
View File
@@ -201,6 +201,11 @@ int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t*
// family: AF_INET or AF_INET6, returns ifindex (0 on failure)
uint32_t get_default_route_netif_index(int family);
// Классифицирует IPv4-адрес (network byte order):
// 1 = глобальный/публичный (маршрутизируемый в интернете),
// 0 = приватный, CGNAT, loopback, link-local, multicast или зарезервированный.
int ip_is_public(uint32_t addr_be);
// Cross-platform mkdir
#ifdef _WIN32
+25 -7
View File
@@ -79,7 +79,7 @@ struct tcp_conn* tcp_conn_create(
queue_set_callback(tc->write_queue, write_queue_fetch_cb, tc);
queue_set_waiter_defer(tc->write_queue, 1);
tc->socket_id = uasync_add_socket_t(ua, sock, read_cb, write_cb, error_cb, tc);
tc->socket_id = uasync_add_socket_t(ua, sock, read_cb, write_cb, error_cb, "tcp_io", tc);
if (!tc->socket_id) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_conn_create: uasync_add_socket_t failed");
queue_free(tc->read_queue);
@@ -282,6 +282,16 @@ static int flush_write_buf(struct tcp_conn* tc) {
return 0;
}
// Последний send может опустошить очередь при уже выключенном POLLOUT.
// Уведомляем потребителя здесь, не рассчитывая на ещё один write event.
static void notify_flushed(struct tcp_conn* tc) {
if (tc->destroyed || tc->write_buf || tc->write_queue->head || !tc->on_flushed) return;
void (*cb)(struct tcp_conn*, void*) = tc->on_flushed;
tc->on_flushed = NULL;
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: flushed fd=%d", (int)tc->sock);
cb(tc, tc->arg);
}
static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
struct tcp_conn* tc = (struct tcp_conn*)arg;
@@ -300,11 +310,7 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
tc->write_monitor = 0;
DEBUG_TRACE(DEBUG_CATEGORY_SOCKET, "tcp_io: wq_fetch POLLOUT OFF fd=%d (queue empty)", (int)tc->sock);
queue_resume_callback(q);
if (tc->on_flushed) {
void (*cb)(struct tcp_conn*, void*) = tc->on_flushed;
tc->on_flushed = NULL;
cb(tc, tc->arg);
}
notify_flushed(tc);
return;
}
@@ -312,7 +318,7 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
if (!e->dgram) {
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: CLOSE fd=%d", (int)tc->sock);
queue_entry_free(e); queue_resume_callback(q);
uasync_remove_socket_t(tc->ua, tc->sock);
if (tc->socket_id) uasync_remove_socket_t(tc->ua, tc->sock);
socket_close_wrapper(tc->sock);
tc->socket_id = NULL; tc->sock = SOCKET_INVALID; tc->closed = 1;
if (tc->on_closed) tc->on_closed(tc, tc->arg);
@@ -334,6 +340,7 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
memory_pool_free(tc->data_pool, e->dgram);
queue_entry_free(e);
queue_resume_callback(q);
notify_flushed(tc);
} else if (n > 0) {
size_t rem = e->len - (size_t)n;
tc->write_buf = memory_pool_alloc(tc->data_pool);
@@ -389,6 +396,8 @@ static void write_cb(socket_t sock, void* arg) {
tc->connected = 1;
if (tc->connect_timer) { uasync_cancel_timeout(tc->ua, tc->connect_timer); tc->connect_timer = NULL; }
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: connect ok fd=%d", (int)tc->sock);
if (tc->on_connected) tc->on_connected(tc, tc->arg);
if (tc->destroyed) return;
} else {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: connect fail fd=%d err=%d", (int)tc->sock, err);
tcp_conn_handle_error(tc, err ? err : -1);
@@ -404,6 +413,7 @@ static void write_cb(socket_t sock, void* arg) {
tc->write_monitor = 0;
}
queue_resume_callback(tc->write_queue);
notify_flushed(tc);
}
}
@@ -420,6 +430,14 @@ static void error_cb(socket_t sock, void* arg) {
// Peer полностью закрыл свой сокет (EPOLLHUP) после того, как FIN уже прочитан (fin_remote=1).
// EPOLLHUP — level-triggered и «always reported»: если оставить сокет в epoll, каждая итерация
// epoll_wait мгновенно вернёт HUP и error_cb зациклится на 100% CPU. Закрываем сокет.
if (tc->fin_remote && tc->fin_local) {
// Обе TCP-половины закрыты, но владелец ещё может передавать ранее прочитанные данные.
// Убираем level-triggered HUP из poll; CLOSE остаётся решением владельца.
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: both FINs fd=%d — detach poll, await owner drain", (int)tc->sock);
if (tc->socket_id) uasync_remove_socket_t(tc->ua, tc->sock);
tc->socket_id = NULL; tc->write_monitor = 0;
return;
}
if (tc->fin_remote) {
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: async HUP (graceful) fd=%d — peer fully closed after FIN, closing",
(int)tc->sock);
+1
View File
@@ -70,6 +70,7 @@ struct tcp_conn {
struct queue_waiter_handle read_waiter;
// Коллбэки
void (*on_connected)(struct tcp_conn* tc, void* arg); // исходящий connect завершён
void (*on_fin)(struct tcp_conn* tc, void* arg); // FIN получен от удалённой стороны (fin_remote=1)
void (*on_fin_sent)(struct tcp_conn* tc, void* arg); // FIN отправлен удалённой стороне (fin_local=1)
void (*on_error)(struct tcp_conn* tc, int err, void* arg); // фатальная ошибка (вызывается после close сокета; обязан вызвать tcp_conn_destroy)
+85 -26
View File
@@ -1,5 +1,10 @@
// uasync.c
// CLOCK_BOOTTIME (Linux) требует _GNU_SOURCE в glibc.
#ifndef _GNU_SOURCE
#define _GNU_SOURCE 1
#endif
#include "u_async.h"
#include "platform_compat.h"
#include "debug_config.h"
@@ -53,6 +58,7 @@ struct socket_node {
socket_t_callback_t write_cbk_sock; // For SOCK type
socket_callback_t except_cbk;
void* user_data;
const char* name; // Строковый идентификатор сокета для диагностики (литерал)
int active; // 1 if socket is active, 0 if freed (for reuse)
int enable_read; // 1 if read monitoring is enabled
int enable_write; // 1 if write monitoring is enabled
@@ -73,7 +79,7 @@ struct socket_array {
static struct socket_array* socket_array_create(int initial_capacity);
static void socket_array_destroy(struct socket_array* sa);
static int socket_array_add(struct socket_array* sa, int fd, socket_callback_t read_cbk, socket_callback_t write_cbk, socket_callback_t except_cbk, void* user_data);
static int socket_array_add(struct socket_array* sa, int fd, socket_callback_t read_cbk, socket_callback_t write_cbk, socket_callback_t except_cbk, const char* name, void* user_data);
static int socket_array_remove(struct socket_array* sa, int fd);
static struct socket_node* socket_array_get(struct socket_array* sa, int fd);
@@ -130,12 +136,8 @@ static void socket_array_destroy(struct socket_array* sa) {
static int socket_array_add_internal(struct socket_array* sa, int fd, socket_t sock, int type,
socket_callback_t read_cbk_fd, socket_callback_t write_cbk_fd,
socket_t_callback_t read_cbk_sock, socket_t_callback_t write_cbk_sock,
socket_callback_t except_cbk, void* user_data) {
socket_callback_t except_cbk, const char* name, void* user_data) {
if (!sa || fd < 0) return -1;
// FD_SETSIZE check only for POSIX systems - Windows sockets can have any value
#ifndef _WIN32
if (fd >= FD_SETSIZE) return -1;
#endif
if (fd >= sa->capacity) {
// Need to resize - double the capacity
int new_capacity = sa->capacity * 2;
@@ -197,6 +199,7 @@ static int socket_array_add_internal(struct socket_array* sa, int fd, socket_t s
sa->sockets[index].write_cbk_sock = write_cbk_sock;
sa->sockets[index].except_cbk = except_cbk;
sa->sockets[index].user_data = user_data;
sa->sockets[index].name = name ? name : "?";
sa->sockets[index].active = 1;
sa->sockets[index].enable_read = (read_cbk_fd != NULL || read_cbk_sock != NULL) ? 1 : 0;
sa->sockets[index].enable_write = (write_cbk_fd != NULL || write_cbk_sock != NULL) ? 1 : 0;
@@ -213,24 +216,24 @@ static int socket_array_add_internal(struct socket_array* sa, int fd, socket_t s
// Wrapper for adding regular file descriptors (pipe, file)
static int socket_array_add(struct socket_array* sa, int fd, socket_callback_t read_cbk,
socket_callback_t write_cbk, socket_callback_t except_cbk, void* user_data) {
socket_callback_t write_cbk, socket_callback_t except_cbk, const char* name, void* user_data) {
return socket_array_add_internal(sa, fd, SOCKET_INVALID, SOCKET_NODE_TYPE_FD,
read_cbk, write_cbk, NULL, NULL, except_cbk, user_data);
read_cbk, write_cbk, NULL, NULL, except_cbk, name, user_data);
}
// Wrapper for adding socket_t (cross-platform sockets)
static int socket_array_add_socket_t(struct socket_array* sa, socket_t sock, socket_t_callback_t read_cbk,
socket_t_callback_t write_cbk, socket_callback_t except_cbk, void* user_data) {
socket_t_callback_t write_cbk, socket_callback_t except_cbk, const char* name, void* user_data) {
// On Windows, SOCKET is UINT_PTR, so we need to handle indexing differently
#ifdef _WIN32
int fd = (int)(intptr_t)sock; // Use socket value as index on Windows (simplified)
if (fd < 0) return -1; // Windows sockets can have any value, only check negative
#else
int fd = sock; // On POSIX, socket_t is int
if (fd < 0 || fd >= FD_SETSIZE) return -1;
if (fd < 0) return -1;
#endif
return socket_array_add_internal(sa, fd, sock, SOCKET_NODE_TYPE_SOCK,
NULL, NULL, read_cbk, write_cbk, except_cbk, user_data);
NULL, NULL, read_cbk, write_cbk, except_cbk, name, user_data);
}
static int socket_array_remove(struct socket_array* sa, int fd) {
@@ -250,6 +253,7 @@ static int socket_array_remove(struct socket_array* sa, int fd) {
sa->sockets[index].write_cbk_sock = NULL;
sa->sockets[index].except_cbk = NULL;
sa->sockets[index].user_data = NULL;
sa->sockets[index].name = NULL;
sa->sockets[index].enable_read = 0;
sa->sockets[index].enable_write = 0;
// fd_to_index[fd] сохраняем — stale epoll события найдут неактивную ноду и пропустятся.
@@ -297,6 +301,22 @@ static struct socket_node* socket_array_get_by_sock(struct socket_array* sa, soc
return &sa->sockets[index];
}
// Предохранитель: снять сокет с мониторинга по fd (SOCK или FD тип), если он
// оказался в HUP/ERR без except-обработчика. Не закрывает fd — только прекращает
// мониторинг, чтобы event loop не ушёл в busy-loop на вечно готовом сокете.
static void socket_force_unregister(struct UASYNC* ua, int fd) {
if (!ua || fd < 0) return;
#if HAS_EPOLL
if (ua->use_epoll && ua->epoll_fd >= 0) {
epoll_ctl(ua->epoll_fd, EPOLL_CTL_DEL, fd, NULL);
}
#endif
if (socket_array_remove(ua->sockets, fd) == 0) {
ua->socket_free_count++;
ua->poll_fds_dirty = 1;
}
}
// Callback to u_free timeout node and update counters
static void timeout_node_free_callback(void* user_data, void* data) {
struct UASYNC* ua = (struct UASYNC*)user_data;
@@ -343,7 +363,11 @@ uint64_t get_time_tb(void) {
#else
uint64_t get_time_tb(void) {
struct timespec ts;
#ifdef __linux__
clock_gettime(CLOCK_BOOTTIME, &ts);
#else
clock_gettime(CLOCK_MONOTONIC, &ts);
#endif
return (uint64_t)ts.tv_sec * 10000ULL + (uint64_t)ts.tv_nsec / 100000ULL; // Преобразуем в требуемые единицы времени
}
#endif
@@ -358,7 +382,11 @@ uint64_t get_time_us(void) {
#else
uint64_t get_time_us(void) {
struct timespec ts;
#ifdef __linux__
clock_gettime(CLOCK_BOOTTIME, &ts);
#else
clock_gettime(CLOCK_MONOTONIC, &ts);
#endif
return (uint64_t)ts.tv_sec * 1000000ULL + (uint64_t)ts.tv_nsec / 1000ULL;
}
#endif
@@ -656,14 +684,10 @@ err_t uasync_cancel_timeout(struct UASYNC* ua, void* t_id) {
// Instance version
void* uasync_add_socket(struct UASYNC* ua, int fd, socket_callback_t read_cbk, socket_callback_t write_cbk, socket_callback_t except_cbk, void* user_data) {
void* uasync_add_socket(struct UASYNC* ua, int fd, socket_callback_t read_cbk, socket_callback_t write_cbk, socket_callback_t except_cbk, const char* name, void* user_data) {
if (!ua || fd < 0) return NULL;
// FD_SETSIZE check only for POSIX systems - Windows sockets can have any value
#ifndef _WIN32
if (fd >= FD_SETSIZE) return NULL;
#endif
int index = socket_array_add(ua->sockets, fd, read_cbk, write_cbk, except_cbk, user_data);
int index = socket_array_add(ua->sockets, fd, read_cbk, write_cbk, except_cbk, name, user_data);
if (index < 0) return NULL;
ua->socket_alloc_count++;
@@ -688,6 +712,9 @@ void* uasync_add_socket(struct UASYNC* ua, int fd, socket_callback_t read_cbk, s
}
#endif
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "socket_add fd=%d name='%s' type=FD r=%p w=%p e=%p ud=%p",
fd, name ? name : "?", (void*)read_cbk, (void*)write_cbk, (void*)except_cbk, user_data);
// Return index-based handle
return (void*)(uintptr_t)(index + 1);
}
@@ -701,6 +728,7 @@ err_t uasync_remove_socket(struct UASYNC* ua, void* s_id) {
if (!node->active || node->fd < 0) return ERR_FAIL;
int fd = node->fd;
const char* rname = node->name ? node->name : "?";
#if HAS_EPOLL
// Remove from epoll if using epoll
@@ -713,6 +741,7 @@ err_t uasync_remove_socket(struct UASYNC* ua, void* s_id) {
if (ret == 0) {
ua->socket_free_count++;
ua->poll_fds_dirty = 1; // Mark poll_fds as needing rebuild
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "socket_remove fd=%d name='%s'", fd, rname);
return ERR_OK;
}
return ERR_FAIL;
@@ -720,11 +749,11 @@ err_t uasync_remove_socket(struct UASYNC* ua, void* s_id) {
// Add socket_t (cross-platform socket)
void* uasync_add_socket_t(struct UASYNC* ua, socket_t sock, socket_t_callback_t read_cbk,
socket_t_callback_t write_cbk, socket_t_callback_t except_cbk, void* user_data) {
socket_t_callback_t write_cbk, socket_t_callback_t except_cbk, const char* name, void* user_data) {
if (!ua || sock == SOCKET_INVALID) return NULL;
int index = socket_array_add_socket_t(ua->sockets, sock, read_cbk, write_cbk,
(socket_callback_t)except_cbk, user_data);
(socket_callback_t)except_cbk, name, user_data);
if (index < 0) return NULL;
ua->socket_alloc_count++;
@@ -751,6 +780,9 @@ void* uasync_add_socket_t(struct UASYNC* ua, socket_t sock, socket_t_callback_t
}
#endif
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "socket_add fd=%d name='%s' type=SOCK r=%p w=%p e=%p ud=%p",
(int)sock, name ? name : "?", (void*)read_cbk, (void*)write_cbk, (void*)except_cbk, user_data);
return (void*)(uintptr_t)(index + 1); /* +1: index 0 ≠ NULL */
}
@@ -760,6 +792,7 @@ err_t uasync_remove_socket_t(struct UASYNC* ua, socket_t sock) {
struct socket_node* node = socket_array_get_by_sock(ua->sockets, sock);
if (!node || !node->active) return ERR_FAIL;
const char* rname = node->name ? node->name : "?";
#if HAS_EPOLL
if (ua->use_epoll && ua->epoll_fd >= 0) {
@@ -781,6 +814,7 @@ err_t uasync_remove_socket_t(struct UASYNC* ua, socket_t sock) {
if (ret == 0) {
ua->socket_free_count++;
ua->poll_fds_dirty = 1;
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "socket_remove fd=%d name='%s'", fd, rname);
return ERR_OK;
}
return ERR_FAIL;
@@ -991,6 +1025,17 @@ static void process_epoll_events(struct UASYNC* ua, struct epoll_event* events,
if (events[i].events & (EPOLLERR | EPOLLHUP)) {
if (local_except) {
local_except(local_fd, local_ud);
} else {
struct socket_node* n = socket_array_get(ua->sockets, fd);
if (!n) n = socket_array_get_by_sock(ua->sockets, fd);
if (n && n->active && n->gen == ev_gen) {
DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "socket HUP/ERR without except handler: fd=%d name='%s' type=%s r=%p w=%p — unregister",
fd, n->name ? n->name : "?",
n->type == SOCKET_NODE_TYPE_SOCK ? "SOCK" : "FD",
(void*)(n->type == SOCKET_NODE_TYPE_SOCK ? (void*)n->read_cbk_sock : (void*)n->read_cbk),
(void*)(n->type == SOCKET_NODE_TYPE_SOCK ? (void*)n->write_cbk_sock : (void*)n->write_cbk));
socket_force_unregister(ua, fd);
}
}
}
}
@@ -1323,6 +1368,13 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) {
/* Treat as exceptional condition */
if (node->except_cbk) {
node->except_cbk(node->fd, node->user_data);
} else {
DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "socket HUP/ERR/NVAL without except handler: fd=%d rev=0x%x name='%s' type=%s r=%p w=%p — unregister",
fd, ua->poll_fds[i].revents, node->name ? node->name : "?",
node->type == SOCKET_NODE_TYPE_SOCK ? "SOCK" : "FD",
(void*)(node->type == SOCKET_NODE_TYPE_SOCK ? (void*)node->read_cbk_sock : (void*)node->read_cbk),
(void*)(node->type == SOCKET_NODE_TYPE_SOCK ? (void*)node->write_cbk_sock : (void*)node->write_cbk));
socket_force_unregister(ua, fd);
}
cb_called++;
}
@@ -1509,7 +1561,7 @@ struct UASYNC* uasync_create(void) {
ioctlsocket(r, FIONBIO, &mode);
// Register the read socket with uasync
uasync_add_socket_t(ua, r, wakeup_read_callback_win, NULL, NULL, ua); // ← ua как user_data
uasync_add_socket_t(ua, r, wakeup_read_callback_win, NULL, NULL, "wakeup", ua); // ← ua как user_data
// uasync_add_socket_t(ua, r, wakeup_read_callback_win, NULL, NULL, NULL);
InitializeCriticalSection(&ua->posted_lock);
@@ -1549,7 +1601,7 @@ struct UASYNC* uasync_create(void) {
#endif
if (!ua->use_epoll) {
uasync_add_socket(ua, ua->wakeup_pipe[0], wakeup_read_callback_posix, NULL, NULL, ua); // ← ua
uasync_add_socket(ua, ua->wakeup_pipe[0], wakeup_read_callback_posix, NULL, NULL, "wakeup", ua); // ← ua
}
pthread_mutex_init(&ua->posted_lock, NULL);
@@ -1607,10 +1659,14 @@ void uasync_print_resources(struct UASYNC* ua, const char* prefix) {
ua->sockets->capacity, ua->sockets->count);
for (int i = 0; i < ua->sockets->capacity; i++) {
if (ua->sockets->sockets[i].active) {
struct socket_node* sn = &ua->sockets->sockets[i];
active_sockets++;
DEBUG_INFO(DEBUG_CATEGORY_SYS, " Socket: fd=%d, active=%d",
ua->sockets->sockets[i].fd,
ua->sockets->sockets[i].active);
DEBUG_INFO(DEBUG_CATEGORY_SYS, " Socket: fd=%d name='%s' type=%s r=%p w=%p e=%p ud=%p",
sn->fd, sn->name ? sn->name : "?",
sn->type == SOCKET_NODE_TYPE_SOCK ? "SOCK" : "FD",
(void*)(sn->type == SOCKET_NODE_TYPE_SOCK ? (void*)sn->read_cbk_sock : (void*)sn->read_cbk),
(void*)(sn->type == SOCKET_NODE_TYPE_SOCK ? (void*)sn->write_cbk_sock : (void*)sn->write_cbk),
(void*)sn->except_cbk, sn->user_data);
}
}
DEBUG_INFO(DEBUG_CATEGORY_SYS, " Total active sockets: %d", active_sockets);
@@ -1719,10 +1775,13 @@ void uasync_destroy(struct UASYNC* ua, int close_fds) {
if (ua->wakeup_initialized) {
#ifdef _WIN32
closesocket((SOCKET)(intptr_t)ua->wakeup_pipe[0]);
closesocket((SOCKET)(intptr_t)ua->wakeup_pipe[1]);
if (ua->wakeup_pipe[1] != ua->wakeup_pipe[0])
closesocket((SOCKET)(intptr_t)ua->wakeup_pipe[1]);
#else
close(ua->wakeup_pipe[0]);
close(ua->wakeup_pipe[1]);
// eventfd на Linux: wakeup_pipe[0] == wakeup_pipe[1] — избегаем двойного close
if (ua->wakeup_pipe[1] != ua->wakeup_pipe[0])
close(ua->wakeup_pipe[1]);
#endif
}
+2 -2
View File
@@ -113,9 +113,9 @@ void* uasync_call_soon(struct UASYNC* ua, void* user_arg, timeout_callback_t cal
err_t uasync_call_soon_cancel(struct UASYNC* ua, void* t_id);
// Sockets - for regular file descriptors (pipe, file)
void* uasync_add_socket(struct UASYNC* ua, int fd, socket_callback_t read_cbk, socket_callback_t write_cbk, socket_callback_t except_cbk, void* user_arg);
void* uasync_add_socket(struct UASYNC* ua, int fd, socket_callback_t read_cbk, socket_callback_t write_cbk, socket_callback_t except_cbk, const char* name, void* user_arg);
// Sockets - for socket_t (cross-platform sockets)
void* uasync_add_socket_t(struct UASYNC* ua, socket_t sock, socket_t_callback_t read_cbk, socket_t_callback_t write_cbk, socket_t_callback_t except_cbk, void* user_arg);
void* uasync_add_socket_t(struct UASYNC* ua, socket_t sock, socket_t_callback_t read_cbk, socket_t_callback_t write_cbk, socket_t_callback_t except_cbk, const char* name, void* user_arg);
err_t uasync_remove_socket(struct UASYNC* ua, void* s_id);
err_t uasync_remove_socket_t(struct UASYNC* ua, socket_t sock);
+1 -1
View File
@@ -425,7 +425,7 @@ int net_emulator_init(net_emulator_t *emulator) {
ep->emulator = emulator;
// Add socket to u_async
uasync_add_socket(emulator->ua, ep->sockfd, socket_callback, NULL, NULL, ep);
uasync_add_socket(emulator->ua, ep->sockfd, socket_callback, NULL, NULL, "emulator", ep);
printf("[INIT] Listening on %s:%u\n",
inet_ntoa(ep->listen_addr.sin_addr), ntohs(ep->listen_addr.sin_port));
}
+20
View File
@@ -30,6 +30,8 @@ utun_CORE_SOURCES = \
tun_freebsd.c \
tun_windows.c \
transport_layer/etcp.c \
transport_layer/etcp_session.c \
transport_layer/etcp_session.h \
transport_layer/etcp_connections.c \
transport_layer/etcp_keepalive.c \
transport_layer/etcp_bbr.c \
@@ -65,6 +67,7 @@ utun_CORE_SOURCES = \
transport_layer/dummynet.c \
ntp_time.c \
ntp_node_time.c \
proxy/proxy_protocol.h \
proxy/tcp_proxy_client.c \
routing_layer/etcp_router.c \
routing_layer/route_crypto.c \
@@ -89,12 +92,18 @@ utun_CORE_SOURCES = \
chat/chat_join.c \
chat/member_sync.c \
chat/merkle_sync.c \
chat/merkle_tree.c \
chat/merkle_tree.h \
chat/invite_link.c \
chat/invite_build.c \
chat/chat_headless_control.c \
dm/dm_core.c \
dm/dm_crypto.c \
dm/dm_mailbox.c \
call/call.c \
call/call_headless.c \
radio/radio.c \
radio/radio_headless.c \
broadcast.c
# libutun: all core sources except main()
@@ -125,6 +134,8 @@ libutun_a_SOURCES = \
tun_freebsd.c \
tun_windows.c \
transport_layer/etcp.c \
transport_layer/etcp_session.c \
transport_layer/etcp_session.h \
transport_layer/etcp_connections.c \
transport_layer/etcp_keepalive.c \
transport_layer/etcp_bbr.c \
@@ -148,6 +159,7 @@ libutun_a_SOURCES = \
transport_layer/node_conn_direct.c \
transport_layer/socket_monitor.c \
transport_layer/auto_socket.c \
transport_layer/call_ring.c \
control_server.c \
firewall.c \
eim_nat.c \
@@ -160,6 +172,7 @@ libutun_a_SOURCES = \
transport_layer/dummynet.c \
ntp_time.c \
ntp_node_time.c \
proxy/proxy_protocol.h \
proxy/tcp_proxy_client.c \
routing_layer/etcp_router.c \
routing_layer/route_crypto.c \
@@ -184,11 +197,17 @@ libutun_a_SOURCES = \
chat/member_sync.c \
chat/merkle_sync.c \
chat/invite_link.c \
chat/merkle_tree.c \
chat/merkle_tree.h \
chat/invite_build.c \
chat/chat_headless_control.c \
dm/dm_core.c \
dm/dm_crypto.c \
dm/dm_mailbox.c \
call/call.c \
call/call_headless.c \
radio/radio.c \
radio/radio_headless.c \
broadcast.c
libutun_a_CFLAGS = $(utun_CFLAGS)
@@ -206,6 +225,7 @@ utun_CFLAGS = \
-I$(top_srcdir)/src/media_delivery \
-I$(top_srcdir)/src/media_async \
-I$(top_srcdir)/src/video \
-I$(top_srcdir)/src/call \
-I$(top_srcdir)/src/uip \
-g \
-DUSE_SQLITE \
+1009
View File
File diff suppressed because it is too large. Load diff
+98
View File
@@ -0,0 +1,98 @@
// call.h — P2P аудио-звонок (сигналинг + медиа), per-instance
//
// Модуль оперирует (group_id, peer_node_id): звонок можно инициировать из
// CHAT-группы или из DM (group_id — группа маршрутизации, как в dm_core).
// Сигналинг идёт через etcp_router (ROUTE_CRYPTO_SIGN|ENCRYPT), соединение
// устанавливается через conn_mgr (3-фазное DIRECT→REVERSE→INDIRECT).
//
// Все функции — в uasync-потоке. GUI/headless вызывают через trampoline
// (gui_bridge_post_uasync_fn / uasync_post).
#ifndef CALL_H
#define CALL_H
#include <stdint.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: начать звонок. 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);
/* 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);
/* Коллбэк приёма медиа: вызывается в 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);
/* 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);
};
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;
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);
#ifdef __cplusplus
}
#endif
#endif /* CALL_H */
+298
View File
@@ -0,0 +1,298 @@
// call_audio.c — сервисный аудио-движок P2P-звонка (Opus + джиттер + тоны).
//
// Один активный звонок: encoder (аудио-поток), decoder + адаптивный джиттер-буфер
// vj (кодированные кадры + SoundTouch stretch на pull) и генератор тонов.
//
// RX: call.c → call_audio_on_media (uasync) → vj_push (кодированный кадр);
// аудио-поток call_audio_pull_pcm → vj_pull (декод+stretch) (+ глитч/завершение).
// TX: аудио-поток call_audio_feed_pcm → encode → uasync_post(call_send_media).
#include "call_audio.h"
#include "call.h"
#include "voice_jitter.h"
#include "call_tones.h"
#include "../utun_instance.h"
#include "../../lib/opus_codec.h"
#include "../../lib/debug_config.h"
#include "../../lib/mem.h"
#include "../../lib/u_async.h"
#include <pthread.h>
#include <string.h>
#define CALL_AUDIO_ID "call_audio"
#define CALL_AUDIO_BITRATE 32000
#define CALL_AJ_MAX_FRAMES 50 /* ~1000мс @20мс */
#define CALL_STALL_TB 5000 /* 500мс без медиа → глитч-тон */
static pthread_mutex_t g_mtx = PTHREAD_MUTEX_INITIALIZER;
static struct UTUN_INSTANCE* g_inst = NULL;
static int g_active = 0;
static int g_ending = 0;
static int g_ended_started = 0; /* только аудио-поток: тон завершения запущен */
static uint64_t g_call_id = 0;
static uint64_t g_last_media_tb = 0;
static opus_codec_encoder_t* g_encoder = NULL;
static opus_codec_decoder_t* g_decoder = NULL;
static struct vj* g_vj = NULL;
static struct call_tones* g_tones = NULL;
static const struct call_audio_ops g_ops = {
.on_media = call_audio_on_media,
.get_stats = call_audio_get_stats,
};
/* vj decode-коллбэк: дёргается только из аудио-потока (vj_pull). */
static int call_audio_decode_cb(void* arg, const uint8_t* enc, int len, int16_t* pcm, int max_samples) {
opus_codec_decoder_t* dec = (opus_codec_decoder_t*)arg;
return opus_codec_decode(dec, enc, len, pcm, max_samples);
}
int call_audio_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
if (g_inst == inst) return 0;
g_inst = inst;
call_set_audio_ops(inst, &g_ops);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized", CALL_AUDIO_ID);
return 0;
}
void call_audio_destroy(struct UTUN_INSTANCE* inst) {
if (!inst || g_inst != inst) return;
call_set_audio_ops(inst, NULL);
g_inst = NULL;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: destroyed", CALL_AUDIO_ID);
}
/* ── uasync-поток: приём медиа (кладём кодированный кадр в jitter-буфер) ── */
void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len) {
(void)inst; (void)seq; (void)ts_ms;
pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id && g_vj && opus && len > 0) {
vj_push(g_vj, opus, len);
g_last_media_tb = get_time_tb();
}
pthread_mutex_unlock(&g_mtx);
}
/* ── GUI-поток: lifecycle ── */
int call_audio_start(uint64_t call_id) {
if (call_id == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: start bad call_id=0", CALL_AUDIO_ID);
return -1;
}
pthread_mutex_lock(&g_mtx);
if (g_active && g_call_id == call_id) {
pthread_mutex_unlock(&g_mtx);
return 0;
}
if (g_active) {
pthread_mutex_unlock(&g_mtx);
call_audio_release(g_call_id);
pthread_mutex_lock(&g_mtx);
}
opus_codec_encoder_t* enc = opus_codec_encoder_create(CALL_AUDIO_SAMPLE_RATE, 1);
if (!enc) {
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: encoder create failed", CALL_AUDIO_ID);
return -1;
}
opus_codec_encoder_bitrate_set(enc, CALL_AUDIO_BITRATE);
opus_codec_decoder_t* dec = opus_codec_decoder_create(CALL_AUDIO_SAMPLE_RATE, 1);
if (!dec) {
opus_codec_encoder_destroy(enc);
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: decoder create failed", CALL_AUDIO_ID);
return -1;
}
struct vj* j = vj_create(CALL_AUDIO_SAMPLE_RATE, 1, CALL_AUDIO_FRAME_SAMPLES, CALL_AUDIO_FRAME_MS,
CALL_AJ_MAX_FRAMES, call_audio_decode_cb, dec);
if (!j) {
opus_codec_decoder_destroy(dec);
opus_codec_encoder_destroy(enc);
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: vj create failed", CALL_AUDIO_ID);
return -1;
}
struct call_tones* tones = call_tones_create();
if (!tones) {
vj_destroy(j);
opus_codec_decoder_destroy(dec);
opus_codec_encoder_destroy(enc);
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: tones create failed", CALL_AUDIO_ID);
return -1;
}
g_encoder = enc;
g_decoder = dec;
g_vj = j;
g_tones = tones;
g_call_id = call_id;
g_active = 1;
g_ending = 0;
g_ended_started = 0;
g_last_media_tb = 0;
pthread_mutex_unlock(&g_mtx);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: started call=0x%016llx", CALL_AUDIO_ID, (unsigned long long)call_id);
return 0;
}
void call_audio_begin_end(uint64_t call_id) {
pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id && !g_ending) {
g_ending = 1;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: begin_end call=0x%016llx (ended tone)", CALL_AUDIO_ID, (unsigned long long)call_id);
}
pthread_mutex_unlock(&g_mtx);
}
void call_audio_release(uint64_t call_id) {
opus_codec_encoder_t* enc = NULL;
opus_codec_decoder_t* dec = NULL;
struct vj* j = NULL;
struct call_tones* tones = NULL;
int was_active = 0;
pthread_mutex_lock(&g_mtx);
if (g_active && g_call_id == call_id) {
was_active = 1;
g_active = 0;
g_ending = 0;
g_ended_started = 0;
g_call_id = 0;
g_last_media_tb = 0;
enc = g_encoder; g_encoder = NULL;
dec = g_decoder; g_decoder = NULL;
j = g_vj; g_vj = NULL;
tones = g_tones; g_tones = NULL;
}
pthread_mutex_unlock(&g_mtx);
if (!was_active) return;
/* дальше on_media не тронет объекты (active=0, указатели NULL) */
if (enc) opus_codec_encoder_destroy(enc);
if (dec) opus_codec_decoder_destroy(dec);
if (j) vj_destroy(j);
if (tones) call_tones_destroy(tones);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: released call=0x%016llx", CALL_AUDIO_ID, (unsigned long long)call_id);
}
void call_audio_stop(void) {
uint64_t cid = 0;
pthread_mutex_lock(&g_mtx);
if (g_active) cid = g_call_id;
pthread_mutex_unlock(&g_mtx);
if (cid) call_audio_release(cid);
}
/* ── аудио-поток: TX / RX ── */
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) {
if (!pcm || count != CALL_AUDIO_FRAME_SAMPLES) return -1;
uint8_t opus[256];
struct UASYNC* ua = NULL;
struct UTUN_INSTANCE* inst = NULL;
int len = -1;
pthread_mutex_lock(&g_mtx);
if (!g_active || call_id != g_call_id || !g_encoder) {
pthread_mutex_unlock(&g_mtx);
return -1;
}
len = opus_codec_encode(g_encoder, pcm, count, opus, (int)sizeof(opus));
ua = g_inst ? g_inst->ua : NULL;
inst = g_inst;
pthread_mutex_unlock(&g_mtx);
if (len <= 0) return -1;
if (!ua || !inst) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: feed_pcm: instance not set (call_audio_init not called)", CALL_AUDIO_ID);
return -1;
}
struct call_media_arg* a = (struct call_media_arg*)u_malloc(sizeof(*a));
if (!a) return -1;
a->inst = inst;
a->call_id = call_id;
a->len = (uint16_t)len;
memcpy(a->opus, opus, (size_t)len);
uasync_post(ua, call_send_media_trampoline, a);
return 0;
}
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
if (!out || max_samples <= 0) return 0;
int active = 0, ending = 0;
struct vj* j = NULL;
struct call_tones* tones = NULL;
uint64_t last_media = 0;
pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id) {
active = 1;
ending = g_ending;
j = g_vj;
tones = g_tones;
last_media = g_last_media_tb;
}
pthread_mutex_unlock(&g_mtx);
if (!active || !j || !tones) return 0;
/* завершение: доигрываем тон (450мс), затем тишина. Тон стартует лениво
* здесь (аудио-поток) — begin_end лишь выставляет флаг, чтобы не гонять
* генератор тонов с GUI-потоком. */
if (ending) {
if (!g_ended_started) {
call_tones_ended_start(tones);
g_ended_started = 1;
}
if (call_tones_ended_pending(tones))
return call_tones_render(tones, out, max_samples);
return 0;
}
int n = vj_pull(j, out, max_samples);
if (n >= max_samples) return n;
/* недобор: либо сталь (глитч-тон), либо тишина */
uint64_t now = get_time_tb();
int stalled = (last_media != 0) && (now - last_media > CALL_STALL_TB);
if (stalled) {
call_tones_glitch_start(tones);
int m = call_tones_render(tones, out + n, max_samples - n);
n += m;
} else {
call_tones_glitch_stop(tones);
}
for (int i = n; i < max_samples; i++) out[i] = 0;
return max_samples;
}
/* ── uasync-поток: метрики ── */
int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100,
uint32_t* dropped, uint32_t* underruns) {
int ok = 0;
pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id && g_vj) {
vj_get_stats(g_vj, buffer_ms, tempo_x100, dropped, underruns);
ok = 1;
}
pthread_mutex_unlock(&g_mtx);
return ok ? 0 : -1;
}
+69
View File
@@ -0,0 +1,69 @@
#ifndef CALL_AUDIO_H
#define CALL_AUDIO_H
// call_audio.h — сервисный аудио-движок P2P-звонка (единый для desktop/Android/headless).
//
// Владеет Opus codec, адаптивным джиттер-буфером (voice_jitter: кодированные
// кадры + SoundTouch time-stretch на pull) и тонами (глитч при сталле / завершение).
// Излишек голоса держится до декодирования — декодируется только то, что готово
// к воспроизведению. Потребитель (desktop miniaudio / Android AudioTrack) получает
// финальный PCM через call_audio_pull_pcm и отдаёт захваченный PCM через
// call_audio_feed_pcm.
//
// Потоки:
// - call_audio_start / begin_end / release — GUI-поток (по ACCEPTED/ENDED);
// - call_audio_feed_pcm / pull_pcm — аудио-поток (single-writer);
// - call_audio_on_media / get_stats — uasync-поток (из call.c).
// Общие указатели (encoder/decoder/vj/tones) защищены мьютексом; encoder и
// decoder — только аудио-поток (decoder дёргается из vj_pull через коллбэк),
// vj_push — только uasync-поток.
#include <stdint.h>
struct UTUN_INSTANCE;
#ifdef __cplusplus
extern "C" {
#endif
#define CALL_AUDIO_SAMPLE_RATE 48000
#define CALL_AUDIO_FRAME_MS 20
#define CALL_AUDIO_FRAME_SAMPLES (CALL_AUDIO_SAMPLE_RATE * CALL_AUDIO_FRAME_MS / 1000) /* 960 */
/* Зарегистрировать аудио-движок в call.c (идемпотентно). */
int call_audio_init(struct UTUN_INSTANCE* inst);
void call_audio_destroy(struct UTUN_INSTANCE* inst);
/* GUI-поток: создать codec + jitter + tones для звонка. 0 = успех. */
int call_audio_start(uint64_t call_id);
/* GUI-поток: по CALL_ENDED — начать тон завершения (pull_pcm выдаст 450мс тона). */
void call_audio_begin_end(uint64_t call_id);
/* GUI-поток (после остановки аудиоустройства): освободить все объекты. */
void call_audio_release(uint64_t call_id);
/* GUI-поток: остановить текущий активный звонок (удобная обёртка release). */
void call_audio_stop(void);
/* аудио-поток: закодировать PCM-кадр (ровно CALL_AUDIO_FRAME_SAMPLES) и отправить пиру. */
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count);
/* аудио-поток: выдать до max_samples финального PCM (jitter + stretch + тоны).
* Возвращает число сэмплов (может быть < max — андерфлоу/конец тона). */
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples);
/* uasync-поток (из call.c): принятый Opus-кадр — кладём в джиттер-буфер (vj_push,
* декодирование произойдёт на pull в аудио-потоке). */
void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len);
/* uasync-поток: метрики джиттер-буфера для CALL_STATS (dropped/underruns — необязательные). */
int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100,
uint32_t* dropped, uint32_t* underruns);
#ifdef __cplusplus
}
#endif
#endif /* CALL_AUDIO_H */
+366
View File
@@ -0,0 +1,366 @@
// call_headless.c — headless-аудио-сокет для P2P-звонков (PCM, без тактирования)
//
// TCP-сокет с бинарным фреймингом [type:1][call_id:8][len:2][data:len].
// Клиент привязывается к звонку (HELLO) и обменивается ГОТОВЫМ PCM (int16 mono 48k):
// клиент → ядро: PCM-кадр (960 сэмплов) — кодируется в Opus и уходит пиру;
// ядро → клиент: Opus пира декодируется в PCM и отдаётся клиенту.
// Тактирование/джиттер — на стороне клиента (ядро лишь кодирует/декодирует).
// Сигналинг — на control-сокете (chat_headless_control), медиа — здесь.
#include "call_headless.h"
#include "call.h"
#include "../utun_instance.h"
#include "../../lib/opus_codec.h"
#include "../../lib/u_async.h"
#include "../../lib/mem.h"
#include "../../lib/socket_compat.h"
#include "../../lib/debug_config.h"
#include "../../lib/platform_compat.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#define CH_ID "call_audio"
#define AUDIO_RECV_BUF_SIZE 65536
#define AUDIO_SEND_BUF_SIZE 65536
#define AUDIO_MAX_CLIENTS 16
#define AUDIO_MAX_FRAME 2048
#define CA_SAMPLE_RATE 48000
#define CA_FRAME_SAMPLES 960 /* 20мс @48k */
#define CA_PCM_FRAME_BYTES (CA_FRAME_SAMPLES * (int)sizeof(int16_t)) /* 1920 */
#define CA_BITRATE 32000
struct call_audio_client {
socket_t fd;
void* socket_id;
uint64_t call_id; /* 0 = не привязан */
uint8_t recv_buf[AUDIO_RECV_BUF_SIZE];
int recv_len;
uint8_t send_buf[AUDIO_SEND_BUF_SIZE];
int send_len;
int send_offset;
int closing;
struct call_audio* ca;
struct call_audio_client* next;
};
struct call_audio {
struct UASYNC* ua;
struct UTUN_INSTANCE* inst;
struct call_audio_client* clients;
int client_count;
socket_t listen_fd;
void* listen_sock_id;
int running;
/* Opus-кодеки (одна активная звонка за раз — busy в call.c) */
uint64_t enc_call_id;
opus_codec_encoder_t* enc;
uint64_t dec_call_id;
opus_codec_decoder_t* dec;
/* диагностика */
uint32_t c_enc_frames;
uint32_t c_dec_frames;
uint32_t c_enc_dropped;
uint32_t c_dec_dropped;
};
static struct call_audio* ca_of(struct UTUN_INSTANCE* inst) {
return inst ? (struct call_audio*)inst->call_audio : NULL;
}
static void ca_close_client(struct call_audio_client* cli);
/* ── отправка фрейма клиенту (с буферизацией как в control-сокете) ── */
static void ca_send_raw(struct call_audio_client* cli, const uint8_t* data, size_t len) {
struct call_audio* ca = cli->ca;
if (!cli || cli->closing || cli->fd == SOCKET_INVALID || !data || len == 0) return;
if (cli->send_len > 0) {
size_t space = sizeof(cli->send_buf) - (size_t)cli->send_len;
if (len > space) return;
memcpy(cli->send_buf + cli->send_len, data, len);
cli->send_len += (int)len;
return;
}
ssize_t r = send(cli->fd, data, (int)len, 0);
if (r < 0 && (socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK)) {
if (len <= sizeof(cli->send_buf)) { memcpy(cli->send_buf, data, len); cli->send_len = (int)len; cli->send_offset = 0; }
if (ca && ca->ua) uasync_set_socket_write(ca->ua, cli->socket_id, 1);
return;
}
if (r >= 0 && (size_t)r < len) {
size_t remain = len - (size_t)r;
if (remain <= sizeof(cli->send_buf)) { memcpy(cli->send_buf, data + r, remain); cli->send_len = (int)remain; cli->send_offset = 0; }
if (ca && ca->ua) uasync_set_socket_write(ca->ua, cli->socket_id, 1);
}
if (r < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: send failed err=%d, closing client", CH_ID, socket_get_error());
ca_close_client(cli);
}
}
static void ca_write_frame(struct call_audio_client* cli, uint8_t type, uint64_t call_id,
const uint8_t* data, uint16_t len) {
uint8_t hdr[CALL_AUDIO_HDR_SIZE];
hdr[0] = type;
memcpy(hdr + 1, &call_id, 8);
hdr[9] = (uint8_t)(len >> 8);
hdr[10] = (uint8_t)(len & 0xFF);
ca_send_raw(cli, hdr, sizeof(hdr));
if (len) ca_send_raw(cli, data, len);
}
/* ── Opus-кодеки (одна активная звонка за раз) ── */
static opus_codec_encoder_t* ca_encoder(struct call_audio* ca, uint64_t call_id) {
if (ca->enc && ca->enc_call_id == call_id) return ca->enc;
if (ca->enc) { opus_codec_encoder_destroy(ca->enc); ca->enc = NULL; }
ca->enc = opus_codec_encoder_create(CA_SAMPLE_RATE, 1);
if (ca->enc) {
opus_codec_encoder_bitrate_set(ca->enc, CA_BITRATE);
ca->enc_call_id = call_id;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: encoder created for call_id=%016llx", CH_ID, (unsigned long long)call_id);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: encoder create failed", CH_ID);
}
return ca->enc;
}
static opus_codec_decoder_t* ca_decoder(struct call_audio* ca, uint64_t call_id) {
if (ca->dec && ca->dec_call_id == call_id) return ca->dec;
if (ca->dec) { opus_codec_decoder_destroy(ca->dec); ca->dec = NULL; }
ca->dec = opus_codec_decoder_create(CA_SAMPLE_RATE, 1);
if (ca->dec) {
ca->dec_call_id = call_id;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: decoder created for call_id=%016llx", CH_ID, (unsigned long long)call_id);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: decoder create failed", CH_ID);
}
return ca->dec;
}
/* ── приём медиа от пира → PCM клиенту ── */
static void ca_media_recv(struct UTUN_INSTANCE* inst, uint64_t call_id,
const uint8_t* opus, int len, void* arg) {
struct call_audio* ca = (struct call_audio*)arg;
(void)inst;
if (!ca || !opus || len <= 0) return;
opus_codec_decoder_t* dec = ca_decoder(ca, call_id);
if (!dec) return;
int16_t pcm[CA_FRAME_SAMPLES];
int n = opus_codec_decode(dec, opus, len, pcm, CA_FRAME_SAMPLES);
if (n <= 0) {
ca->c_dec_dropped++;
return;
}
ca->c_dec_frames++;
for (struct call_audio_client* c = ca->clients; c; c = c->next) {
if (c->call_id == call_id && !c->closing) {
ca_write_frame(c, CALL_AUDIO_FRAME, call_id, (const uint8_t*)pcm, (uint16_t)(n * (int)sizeof(int16_t)));
break;
}
}
}
/* ── разбор входящих фреймов ── */
static void ca_parse_frames(struct call_audio_client* cli) {
struct call_audio* ca = cli->ca;
uint8_t* p = cli->recv_buf;
int avail = cli->recv_len;
int consumed = 0;
while (avail - consumed >= CALL_AUDIO_HDR_SIZE) {
uint8_t type = p[consumed];
uint64_t call_id; memcpy(&call_id, p + consumed + 1, 8);
uint16_t flen = (uint16_t)((p[consumed + 9] << 8) | p[consumed + 10]);
if (flen > AUDIO_MAX_FRAME) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: frame too big %u — closing", CH_ID, flen);
ca_close_client(cli);
return;
}
if (avail - consumed < (int)(CALL_AUDIO_HDR_SIZE + flen)) break; /* нужен ещё кусок */
const uint8_t* data = p + consumed + CALL_AUDIO_HDR_SIZE;
if (type == CALL_AUDIO_HELLO) {
cli->call_id = call_id;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio client bound to call_id=%016llx", CH_ID, (unsigned long long)call_id);
} else if (type == CALL_AUDIO_FRAME) {
if (flen != CA_PCM_FRAME_BYTES) {
ca->c_enc_dropped++;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: bad PCM frame size=%u (expect %d) — dropped",
CH_ID, flen, CA_PCM_FRAME_BYTES);
} else {
opus_codec_encoder_t* enc = ca_encoder(ca, call_id);
uint8_t opus[256];
int olen = enc ? opus_codec_encode(enc, (const int16_t*)data, CA_FRAME_SAMPLES, opus, (int)sizeof(opus)) : -1;
if (olen <= 0) {
ca->c_enc_dropped++;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: encode failed (olen=%d) call_id=%016llx",
CH_ID, olen, (unsigned long long)call_id);
} else {
ca->c_enc_frames++;
if (call_send_media(ca->inst, call_id, opus, olen) != 0)
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: send_media failed call_id=%016llx len=%d",
CH_ID, (unsigned long long)call_id, olen);
}
}
} else {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: unknown frame type=0x%02x — closing", CH_ID, type);
ca_close_client(cli);
return;
}
consumed += CALL_AUDIO_HDR_SIZE + flen;
}
if (consumed > 0) {
memmove(cli->recv_buf, cli->recv_buf + consumed, (size_t)(cli->recv_len - consumed));
cli->recv_len -= consumed;
}
}
/* ── socket callbacks ── */
static void ca_write_callback(socket_t fd, void* arg) {
struct call_audio_client* cli = (struct call_audio_client*)arg;
struct call_audio* ca = cli ? cli->ca : NULL;
if (!cli || cli->closing || fd != cli->fd) return;
if (cli->send_len <= 0) { if (ca && ca->ua) uasync_set_socket_write(ca->ua, cli->socket_id, 0); return; }
ssize_t r = send(cli->fd, cli->send_buf + cli->send_offset, cli->send_len - cli->send_offset, 0);
if (r < 0 && (socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK)) return;
if (r <= 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: send failed (r=%zd err=%d) — closing client", CH_ID, r, socket_get_error());
ca_close_client(cli);
return;
}
cli->send_offset += (int)r;
if (cli->send_offset >= cli->send_len) { cli->send_len = 0; cli->send_offset = 0; if (ca && ca->ua) uasync_set_socket_write(ca->ua, cli->socket_id, 0); }
}
static void ca_close_client(struct call_audio_client* cli) {
struct call_audio* ca = cli ? cli->ca : NULL;
if (!cli || cli->closing) return;
cli->closing = 1;
if (ca && ca->ua && cli->socket_id) uasync_remove_socket_t(ca->ua, cli->fd);
if (cli->fd != SOCKET_INVALID) { socket_close_wrapper(cli->fd); cli->fd = SOCKET_INVALID; }
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio client closed call_id=%016llx", CH_ID, (unsigned long long)cli->call_id);
}
static void ca_read_callback(socket_t fd, void* arg) {
struct call_audio_client* cli = (struct call_audio_client*)arg;
if (!cli || cli->closing) return;
uint8_t tmp[4096];
ssize_t r = recv(fd, tmp, (int)sizeof(tmp), 0);
if (r <= 0) { ca_close_client(cli); return; }
if (cli->recv_len + (int)r > AUDIO_RECV_BUF_SIZE) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: recv buffer overflow — closing", CH_ID);
ca_close_client(cli);
return;
}
memcpy(cli->recv_buf + cli->recv_len, tmp, (size_t)r);
cli->recv_len += (int)r;
ca_parse_frames(cli);
}
static void ca_accept_callback(socket_t fd, void* arg) {
struct call_audio* ca = (struct call_audio*)arg;
struct sockaddr_storage addr; socklen_t alen = sizeof(addr);
socket_t cfd = accept(fd, (struct sockaddr*)&addr, &alen);
if (cfd == SOCKET_INVALID) {
int e = socket_get_error();
if (e != ERR_AGAIN && e != ERR_WOULDBLOCK) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: accept failed: %s", CH_ID, socket_strerror(e));
return;
}
socket_set_nonblocking(cfd);
if (ca->client_count >= AUDIO_MAX_CLIENTS) { socket_close_wrapper(cfd); DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: max clients reached", CH_ID); return; }
struct call_audio_client* cli = u_calloc(1, sizeof(*cli));
if (!cli) { socket_close_wrapper(cfd); return; }
cli->fd = cfd;
cli->ca = ca;
cli->socket_id = uasync_add_socket_t(ca->ua, cfd, ca_read_callback, ca_write_callback, NULL, "call_client", cli);
if (!cli->socket_id) { u_free(cli); socket_close_wrapper(cfd); DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: failed to register client fd", CH_ID); return; }
cli->next = ca->clients; ca->clients = cli; ca->client_count++;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio client connected fd=%d total=%d", CH_ID, (int)cfd, ca->client_count);
}
/* ── периодическая очистка закрытых клиентов ── */
static void ca_cleanup(void* arg) {
struct call_audio* ca = (struct call_audio*)arg;
struct call_audio_client** prev = &ca->clients;
while (*prev) {
if ((*prev)->closing) {
struct call_audio_client* dead = *prev; *prev = dead->next;
if (dead->fd != SOCKET_INVALID) socket_close_wrapper(dead->fd);
u_free(dead); ca->client_count--;
} else prev = &(*prev)->next;
}
if (ca->ua) uasync_set_timeout(ca->ua, 5000, ca, ca_cleanup, "ca_cleanup");
}
/* ── init / destroy ── */
int call_headless_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
const char* bind_ip, int port) {
if (!ua || !inst || !bind_ip || port <= 0 || port > 65535) return -1;
struct call_audio* ca = ca_of(inst);
if (ca && ca->running) return 0;
if (!ca) {
ca = u_calloc(1, sizeof(*ca));
if (!ca) return -1;
inst->call_audio = ca;
}
ca->ua = ua; ca->inst = inst;
ca->listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (ca->listen_fd == SOCKET_INVALID) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: socket() failed: %s", CH_ID, socket_strerror(socket_get_error())); return -1; }
socket_set_reuseaddr(ca->listen_fd, 1);
socket_set_nonblocking(ca->listen_fd);
struct sockaddr_in sin; memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET; sin.sin_port = htons((uint16_t)port);
if (inet_pton(AF_INET, bind_ip, &sin.sin_addr) != 1) { socket_close_wrapper(ca->listen_fd); ca->listen_fd = SOCKET_INVALID; return -1; }
if (bind(ca->listen_fd, (struct sockaddr*)&sin, sizeof(sin)) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: bind(%s:%d) failed: %s", CH_ID, bind_ip, port, socket_strerror(socket_get_error()));
socket_close_wrapper(ca->listen_fd); ca->listen_fd = SOCKET_INVALID; return -1;
}
if (listen(ca->listen_fd, 5) < 0) { socket_close_wrapper(ca->listen_fd); ca->listen_fd = SOCKET_INVALID; return -1; }
ca->listen_sock_id = uasync_add_socket_t(ua, ca->listen_fd, ca_accept_callback, NULL, NULL, "call_listen", ca);
if (!ca->listen_sock_id) { socket_close_wrapper(ca->listen_fd); ca->listen_fd = SOCKET_INVALID; return -1; }
call_set_media_recv_cb(inst, ca_media_recv, ca);
uasync_set_timeout(ua, 5000, ca, ca_cleanup, "ca_cleanup");
ca->running = 1;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket listening on %s:%d", CH_ID, bind_ip, port);
return 0;
}
void call_headless_destroy(struct UTUN_INSTANCE* inst) {
struct call_audio* ca = ca_of(inst);
if (!ca || !ca->running) return;
ca->running = 0;
call_set_media_recv_cb(inst, NULL, NULL);
if (ca->listen_sock_id && ca->ua) uasync_remove_socket_t(ca->ua, ca->listen_fd);
if (ca->listen_fd != SOCKET_INVALID) { socket_close_wrapper(ca->listen_fd); ca->listen_fd = SOCKET_INVALID; }
if (ca->enc) { opus_codec_encoder_destroy(ca->enc); ca->enc = NULL; }
if (ca->dec) { opus_codec_decoder_destroy(ca->dec); ca->dec = NULL; }
struct call_audio_client* c = ca->clients;
while (c) {
struct call_audio_client* n = c->next;
if (c->fd != SOCKET_INVALID) socket_close_wrapper(c->fd);
u_free(c);
c = n;
}
ca->clients = NULL; ca->client_count = 0;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket destroyed (enc=%u dec=%u enc_drop=%u dec_drop=%u)",
CH_ID, ca->c_enc_frames, ca->c_dec_frames, ca->c_enc_dropped, ca->c_dec_dropped);
}
+38
View File
@@ -0,0 +1,38 @@
// call_headless.h — headless-аудио-сокет для P2P-звонков (PCM, без тактирования)
//
// Отдельный TCP-сокет, через который headless-клиент (CLI/скрипт) обменивается
// ГОТОВЫМ PCM с ядром звонка. Ядро кодирует PCM→Opus (TX) и декодирует Opus→PCM
// (RX); тактирование и джиттер — на стороне клиента. Сигналинг идёт по
// control-сокету (chat_headless_control), аудио — по этому сокету.
//
// Бинарный фрейм (обе стороны):
// [type:1][call_id:8][len:2][data:len]
// type = 0x01 HELLO (клиент → ядро: привязать соединение к call_id, data пусто)
// type = 0x02 FRAME (PCM int16 mono 48k, 960 сэмплов = 1920 байт)
// клиент → ядро = PCM захвата (кодируется и шлётся пиру);
// ядро → клиент = PCM воспроизведения (декодированный Opus пира)
#ifndef CALL_HEADLESS_H
#define CALL_HEADLESS_H
#include <stdint.h>
struct UASYNC;
struct UTUN_INSTANCE;
#define CALL_AUDIO_HELLO 0x01
#define CALL_AUDIO_FRAME 0x02
#define CALL_AUDIO_HDR_SIZE 11 /* type(1) + call_id(8) + len(2) */
#define CALL_AUDIO_PCM_FRAME_BYTES (960 * (int)sizeof(int16_t)) /* 1920 — PCM int16 mono 48k 20мс */
#ifdef __cplusplus
extern "C" {
#endif
int call_headless_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
const char* bind_ip, int port);
void call_headless_destroy(struct UTUN_INSTANCE* inst);
#ifdef __cplusplus
}
#endif
#endif /* CALL_HEADLESS_H */
+98
View File
@@ -0,0 +1,98 @@
// call_proto.h — wire-формат и константы P2P аудио-звонка (svc_id ETCP_RT_ID_CALL 0x36)
//
// Приём/отправка через etcp_router. На проводе первый байт payload = subcmd.
// Сигналинг идёт с ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT, медиа — без подписи
// (уже внутри шифрованного канала etcp_router).
#ifndef CALL_PROTO_H
#define CALL_PROTO_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* ── подкоманды (первый байт payload после svc_id) ── */
enum {
CALL_SUBCMD_INVITE = 0x01, /* {call_id:8, group_id:8, tracks:[]} */
CALL_SUBCMD_RINGING = 0x02, /* {call_id:8} */
CALL_SUBCMD_ACCEPT = 0x03, /* {call_id:8} */
CALL_SUBCMD_DECLINE = 0x04, /* {call_id:8, reason:1} */
CALL_SUBCMD_BUSY = 0x05, /* {call_id:8} */
CALL_SUBCMD_CANCEL = 0x06, /* {call_id:8} */
CALL_SUBCMD_HANGUP = 0x07, /* {call_id:8, reason:1} */
CALL_SUBCMD_MEDIA = 0x10, /* {call_id:8, track_id:1, seq:2, ts_ms:4, payload[]} */
};
/* ── треки и кодеки (модель расширяема под видео) ── */
enum { CALL_TRACK_AUDIO = 0x01, CALL_TRACK_VIDEO = 0x02 };
enum { CALL_CODEC_OPUS = 0x01 };
/* ── причины завершения (ENDED) ── */
enum {
CALL_REASON_LOCAL_HANGUP = 0,
CALL_REASON_REMOTE_HANGUP = 1,
CALL_REASON_DECLINE = 2,
CALL_REASON_BUSY = 3,
CALL_REASON_CANCEL = 4,
CALL_REASON_NO_TRAFFIC = 5,
CALL_REASON_RING_TIMEOUT = 6,
CALL_REASON_CONNECT_FAIL = 7,
};
#pragma pack(push, 1)
struct call_pkt_track {
uint8_t type; /* CALL_TRACK_AUDIO / CALL_TRACK_VIDEO */
uint8_t codec; /* CALL_CODEC_OPUS */
uint32_t sample_rate;
uint8_t channels;
uint32_t bitrate;
};
struct call_pkt_invite {
uint8_t subcmd; /* CALL_SUBCMD_INVITE */
uint64_t call_id;
uint64_t group_id;
uint8_t num_tracks;
struct call_pkt_track tracks[1]; /* tracks[num_tracks] */
};
struct call_pkt_ringing { uint8_t subcmd; uint64_t call_id; };
struct call_pkt_accept { uint8_t subcmd; uint64_t call_id; };
struct call_pkt_decline { uint8_t subcmd; uint64_t call_id; uint8_t reason; };
struct call_pkt_busy { uint8_t subcmd; uint64_t call_id; };
struct call_pkt_cancel { uint8_t subcmd; uint64_t call_id; };
struct call_pkt_hangup { uint8_t subcmd; uint64_t call_id; uint8_t reason; };
struct call_pkt_media {
uint8_t subcmd; /* CALL_SUBCMD_MEDIA */
uint64_t call_id;
uint8_t track_id;
uint16_t seq;
uint32_t ts_ms;
/* payload[] */
};
#pragma pack(pop)
#define CALL_MEDIA_HDR_SIZE sizeof(struct call_pkt_media) /* 16 */
/* ── параметры аудио (Opus 48kHz mono, 20ms) ── */
#define CALL_OPUS_SAMPLE_RATE 48000
#define CALL_OPUS_FRAME_MS 20
#define CALL_OPUS_FRAME_SAMPLES (CALL_OPUS_SAMPLE_RATE * CALL_OPUS_FRAME_MS / 1000) /* 960 */
/* ── таймауты и ёмкости (timebase 0.1ms) ── */
#define CALL_RING_TIMEOUT_TB 450000 /* 45с — вызывающий: нет ответа → CANCEL */
#define CALL_INVITE_RETRY_TB 3000 /* 300мс — кодограмма INVITE (OUTGOING + RINGING) */
#define CALL_RING_DEAD_TB 20000 /* 2с — вызываемый: нет кодограммы → вызов сорван */
#define CALL_NO_ROUTE_MAX_STRIKES 20 /* 20 × 300мс ≈ 6с без маршрута → fail-fast CONNECT_FAIL */
#define CALL_NO_TRAFFIC_TB 200000 /* 20с — нет входящих медиа → HANGUP */
#define CALL_TEARDOWN_DELAY_TB 5000 /* 500мс — задержка разрыва conn_mgr после завершения */
#define CALL_MAX_TX_FRAMES 50 /* ~1с буфер отправки (50 кадров × 20мс) */
#ifdef __cplusplus
}
#endif
#endif /* CALL_PROTO_H */
+165
View File
@@ -0,0 +1,165 @@
// call_tones.c — генератор тонов звонка (48kHz mono int16).
//
// Глитч (сталл >500мс): 1000Гц, паттерн из doc/chat_call_arch.md §8 —
// пи-пи (2×150мс, gap 100мс) … тишина до 1.0с … пи(200мс) пауза(200мс) пи(200мс),
// цикл 2с, повторяется пока длится сталл.
// Завершение: 3 нисходящих тона 700/500/400 Гц × 150мс (один раз, 450мс).
//
// Генерация — синус с коротким fade in/out (без кликов). Только аудио-поток.
#include "call_tones.h"
#include <math.h>
#include <string.h>
#include <stdlib.h>
#define TONES_SAMPLE_RATE 48000
#define TONES_AMPLITUDE 0.25f /* от полной шкалы int16 */
#define TONES_FADE_SAMPLES (TONES_SAMPLE_RATE * 5 / 1000) /* 5мс */
enum tones_mode { TONES_NONE = 0, TONES_GLITCH, TONES_ENDED };
struct tone_seg {
int tone; /* 1 = звук, 0 = тишина */
int dur_ms;
int freq_hz;
};
static const struct tone_seg GLITCH_SEGS[] = {
{1, 150, 1000}, {0, 100, 0}, {1, 150, 1000}, {0, 600, 0},
{1, 200, 1000}, {0, 200, 0}, {1, 200, 1000}, {0, 400, 0},
};
#define GLITCH_SEG_COUNT ((int)(sizeof(GLITCH_SEGS) / sizeof(GLITCH_SEGS[0])))
static const struct tone_seg ENDED_SEGS[] = {
{1, 150, 700}, {1, 150, 500}, {1, 150, 400},
};
#define ENDED_SEG_COUNT ((int)(sizeof(ENDED_SEGS) / sizeof(ENDED_SEGS[0])))
struct call_tones {
enum tones_mode mode;
const struct tone_seg* segs;
int seg_count;
int seg_idx;
int seg_pos; /* сэмплов отыграно в текущем сегменте */
int ended_done;
double phase; /* фаза синуса (радианы) */
};
struct call_tones* call_tones_create(void) {
struct call_tones* t = (struct call_tones*)calloc(1, sizeof(*t));
if (!t) return NULL;
t->mode = TONES_NONE;
t->segs = NULL;
t->seg_count = 0;
t->seg_idx = 0;
t->seg_pos = 0;
t->ended_done = 0;
t->phase = 0.0;
return t;
}
void call_tones_destroy(struct call_tones* t) {
free(t);
}
void call_tones_reset(struct call_tones* t) {
if (!t) return;
t->mode = TONES_NONE;
t->segs = NULL;
t->seg_count = 0;
t->seg_idx = 0;
t->seg_pos = 0;
t->ended_done = 0;
t->phase = 0.0;
}
void call_tones_glitch_start(struct call_tones* t) {
if (!t || t->mode == TONES_GLITCH) return;
t->mode = TONES_GLITCH;
t->segs = GLITCH_SEGS;
t->seg_count = GLITCH_SEG_COUNT;
t->seg_idx = 0;
t->seg_pos = 0;
t->phase = 0.0;
}
void call_tones_glitch_stop(struct call_tones* t) {
if (!t || t->mode != TONES_GLITCH) return;
t->mode = TONES_NONE;
t->segs = NULL;
t->seg_count = 0;
t->seg_idx = 0;
t->seg_pos = 0;
t->phase = 0.0;
}
void call_tones_ended_start(struct call_tones* t) {
if (!t) return;
t->mode = TONES_ENDED;
t->segs = ENDED_SEGS;
t->seg_count = ENDED_SEG_COUNT;
t->seg_idx = 0;
t->seg_pos = 0;
t->ended_done = 0;
t->phase = 0.0;
}
int call_tones_ended_pending(struct call_tones* t) {
return t && t->mode == TONES_ENDED && !t->ended_done;
}
/* Перейти к следующему сегменту (или завершить тон). */
static void tones_advance(struct call_tones* t) {
t->seg_idx++;
t->seg_pos = 0;
t->phase = 0.0;
if (t->seg_idx >= t->seg_count) {
if (t->mode == TONES_GLITCH) {
t->seg_idx = 0; /* цикл глитча */
} else if (t->mode == TONES_ENDED) {
t->ended_done = 1;
t->mode = TONES_NONE;
t->segs = NULL;
}
}
}
int call_tones_render(struct call_tones* t, int16_t* out, int max_samples) {
if (!t || !out || max_samples <= 0) return 0;
int written = 0;
while (written < max_samples) {
if (t->mode == TONES_NONE) {
out[written++] = 0; /* тишина до конца запроса */
continue;
}
const struct tone_seg* s = &t->segs[t->seg_idx];
int seg_len = s->dur_ms * TONES_SAMPLE_RATE / 1000;
int remain = seg_len - t->seg_pos;
int n = max_samples - written;
if (n > remain) n = remain;
for (int i = 0; i < n; i++) {
int pos = t->seg_pos + i;
float v = 0.0f;
if (s->tone) {
v = (float)sin(t->phase) * TONES_AMPLITUDE * 32767.0f;
t->phase += 2.0 * M_PI * (double)s->freq_hz / TONES_SAMPLE_RATE;
/* fade in/out по краям сегмента (без кликов) */
int fade = pos < TONES_FADE_SAMPLES ? pos
: (seg_len - pos - 1) < TONES_FADE_SAMPLES ? (seg_len - pos - 1)
: TONES_FADE_SAMPLES;
if (fade < TONES_FADE_SAMPLES)
v *= (float)fade / (float)TONES_FADE_SAMPLES;
}
int iv = (int)v;
out[written + i] = (int16_t)(iv > 32767 ? 32767 : (iv < -32768 ? -32768 : iv));
}
written += n;
t->seg_pos += n;
if (t->seg_pos >= seg_len) tones_advance(t);
}
return written;
}
+32
View File
@@ -0,0 +1,32 @@
#ifndef CALL_TONES_H
#define CALL_TONES_H
// call_tones.h — генератор тонов звонка (48kHz mono int16): глитч при сталле
// и тон завершения. Используется только аудио-потоком (single-writer).
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct call_tones;
struct call_tones* call_tones_create(void);
void call_tones_destroy(struct call_tones* t);
void call_tones_reset(struct call_tones* t); /* в тишину */
void call_tones_glitch_start(struct call_tones* t); /* запустить/продолжить цикл глитча */
void call_tones_glitch_stop(struct call_tones* t); /* прекратить глитч (тишина) */
void call_tones_ended_start(struct call_tones* t); /* 3 нисходящих тона, один раз */
int call_tones_ended_pending(struct call_tones* t); /* 1 = тон завершения ещё звучит */
/* Выдать до max_samples сэмплов (тон или тишина). Возвращает записанное число. */
int call_tones_render(struct call_tones* t, int16_t* out, int max_samples);
#ifdef __cplusplus
}
#endif
#endif /* CALL_TONES_H */
+311
View File
@@ -0,0 +1,311 @@
// voice_jitter.cpp — адаптивный джиттер-буфер голоса с time-stretch (SoundTouch).
//
// Кодированные кадры (Opus, до VJ_MAX_ENC байт) кладутся из uasync-потока в кольцо
// и вытаскиваются аудио-потоком: кадр декодируется (decode-коллбэк) и подаётся в
// SoundTouch с плавно меняющимся темпом вокруг целевой глубины: мельче target — темп
// падает к min_tempo (накопление), глубже — растёт к max_tempo (догон live). При
// pre_roll=1 звук не отдаётся, пока глубина не достигнет target_ms. Декодируется
// только то, что готово к воспроизведению.
// Кольцо защищено мьютексом (SPSC); decode и SoundTouch трогает только аудио-поток.
#include "voice_jitter.h"
#include "soundtouch/SoundTouch.h"
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstring>
#include <mutex>
#include <vector>
static const int VJ_MAX_ENC = 512; /* макс. размер кодированного кадра (Opus 20мс, стерео) */
static const double kDefaultTargetMs = 60.0; /* целевая глубина буфера (звонок) */
static const double kDefaultMinTempo = 1.0;
static const double kDefaultMaxTempo = 1.6;
static const double kMaxDepthMs = 1000.0; /* при этой глубине tempo == max_tempo */
static const double kEmaAlpha = 0.2; /* сглаживание глубины */
static const double kTempoStep = 0.01; /* макс. изменение темпа за один pull (без кликов) */
static const double kPrimeTimeoutMs = 200.0; /* pre-roll: принудительный prime через это время */
static const double kStallGapMs = 80.0; /* gap между pull > это → сталь аудио-девайса */
static const double kResyncDepthMs = 400.0; /* бэклог > это после сталла → хард-ресинк к live */
struct vj {
int sr = 0;
int ch = 0;
int frame_samples = 0;
int frame_ms = 0;
int cap = 0;
vj_decode_fn decode = nullptr;
void* decode_arg = nullptr;
std::mutex mtx;
std::vector<uint8_t> ring; /* cap * VJ_MAX_ENC — кодированные кадры */
std::vector<int> lens; /* cap — длины кадров */
int head = 0;
int count = 0;
uint32_t dropped = 0;
uint32_t underruns = 0;
uint32_t resyncs = 0;
/* детект сталла аудио-девайса (gap между pull) */
std::chrono::steady_clock::time_point last_pull_tp; /* только аудио-поток */
int first_pull = 1; /* только аудио-поток */
int stalled = 0; /* только аудио-поток */
soundtouch::SoundTouch st; /* только аудио-поток */
uint8_t enc_buf[VJ_MAX_ENC]; /* scratch: вытащенный кодированный кадр */
std::vector<int16_t> pcm_buf; /* scratch: декодированный кадр */
std::vector<float> fin; /* scratch: кадр в float */
std::vector<float> fout; /* scratch: выход float */
double target_ms = kDefaultTargetMs;
double min_tempo = kDefaultMinTempo;
double max_tempo = kDefaultMaxTempo;
int pre_roll = 0;
int primed = 0; /* только аудио-поток */
int prime_started = 0; /* только аудио-поток: первый непустой pull */
std::chrono::steady_clock::time_point prime_t0; /* только аудио-поток: старт pre-roll */
double ema_depth_ms = 0.0;
double tempo = 1.0;
std::atomic<int> tempo_x100{100};
};
/* Симметричный целевой темп с мёртвой зоной ±25% вокруг target: внутри зоны темп=1.0
* (без time-stretch, меньше артефактов), за её пределами — плавно к min/max. */
static double vj_compute_tempo(double depth_ms, double target_ms, double min_tempo, double max_tempo) {
double dead = target_ms * 0.25;
double lo = target_ms - dead;
double hi = target_ms + dead;
if (depth_ms >= lo && depth_ms <= hi) return 1.0;
if (depth_ms < lo) {
double x = depth_ms / lo; /* 0 → min, lo → 1.0 */
if (x < 0.0) x = 0.0;
return min_tempo + (1.0 - min_tempo) * x;
}
double x = (depth_ms - hi) / (kMaxDepthMs - hi); /* hi → 1.0, kMaxDepth → max */
if (x > 1.0) x = 1.0;
return 1.0 + (max_tempo - 1.0) * x;
}
/* Вытащить один кодированный кадр из кольца (под мьютексом). Возвращает 0 если пусто. */
static int vj_pop_enc(struct vj* j, uint8_t* out, int* len_out) {
std::lock_guard<std::mutex> lk(j->mtx);
if (j->count == 0) return 0;
*len_out = j->lens[(size_t)j->head];
std::memcpy(out, &j->ring[(size_t)j->head * VJ_MAX_ENC], (size_t)*len_out);
j->head = (j->head + 1) % j->cap;
j->count--;
return 1;
}
extern "C" {
struct vj* vj_create_cfg(const struct vj_cfg* cfg) {
if (!cfg || cfg->sample_rate <= 0 || cfg->channels <= 0 || cfg->frame_samples <= 0 ||
cfg->frame_ms <= 0 || cfg->max_frames <= 0 || !cfg->decode)
return nullptr;
if (cfg->min_tempo <= 0.0 || cfg->max_tempo < cfg->min_tempo || cfg->target_ms <= 0.0)
return nullptr;
vj* j = new vj();
j->sr = cfg->sample_rate;
j->ch = cfg->channels;
j->frame_samples = cfg->frame_samples;
j->frame_ms = cfg->frame_ms;
j->cap = cfg->max_frames;
j->decode = cfg->decode;
j->decode_arg = cfg->decode_arg;
j->target_ms = cfg->target_ms;
j->min_tempo = cfg->min_tempo;
j->max_tempo = cfg->max_tempo;
j->pre_roll = cfg->pre_roll;
j->ring.assign((size_t)cfg->max_frames * VJ_MAX_ENC, 0);
j->lens.assign(cfg->max_frames, 0);
j->pcm_buf.assign((size_t)cfg->frame_samples * cfg->channels, 0);
j->fin.assign((size_t)cfg->frame_samples * cfg->channels, 0.0f);
j->st.setSampleRate((uint)cfg->sample_rate);
j->st.setChannels((uint)cfg->channels);
j->st.setTempo(cfg->min_tempo);
j->st.setSetting(SETTING_USE_QUICKSEEK, 1);
j->st.setSetting(SETTING_SEQUENCE_MS, 20);
j->st.setSetting(SETTING_SEEKWINDOW_MS, 10);
j->st.setSetting(SETTING_OVERLAP_MS, 4);
j->st.setSetting(SETTING_USE_AA_FILTER, 0);
return j;
}
struct vj* vj_create(int sample_rate, int channels, int frame_samples, int frame_ms,
int max_frames, vj_decode_fn decode, void* decode_arg) {
struct vj_cfg cfg;
cfg.sample_rate = sample_rate;
cfg.channels = channels;
cfg.frame_samples = frame_samples;
cfg.frame_ms = frame_ms;
cfg.max_frames = max_frames;
cfg.target_ms = kDefaultTargetMs;
cfg.min_tempo = kDefaultMinTempo;
cfg.max_tempo = kDefaultMaxTempo;
cfg.pre_roll = 0;
cfg.decode = decode;
cfg.decode_arg = decode_arg;
return vj_create_cfg(&cfg);
}
void vj_destroy(struct vj* j) {
if (!j) return;
delete j;
}
void vj_push(struct vj* j, const uint8_t* enc, int len) {
if (!j || !enc || len <= 0 || len > VJ_MAX_ENC) return;
std::lock_guard<std::mutex> lk(j->mtx);
if (j->count == j->cap) {
j->head = (j->head + 1) % j->cap; /* дроп старейшего */
j->count--;
j->dropped++;
}
int idx = (j->head + j->count) % j->cap;
std::memcpy(&j->ring[(size_t)idx * VJ_MAX_ENC], enc, (size_t)len);
j->lens[(size_t)idx] = len;
j->count++;
}
/* Хард-ресинк к live: сбросить бэклог (кольцо + SoundTouch) и темп к 1.0.
* Применяется после сталла аудио-девайса, когда бэклог > kResyncDepthMs — один
* чистый срез вместо долгого догона темпом (chipmunk) + дропов по одному. */
static void vj_hard_resync(struct vj* j) {
{ std::lock_guard<std::mutex> lk(j->mtx); j->count = 0; j->head = 0; }
j->st.clear();
j->tempo = 1.0;
j->tempo_x100.store(100);
j->ema_depth_ms = j->target_ms;
j->resyncs++;
}
int vj_pull(struct vj* j, int16_t* out, int max_samples) {
if (!j || !out || max_samples <= 0) return 0;
/* ── детект сталла аудио-девайса (gap между pull) + ресинк по его окончании ── */
{
auto now = std::chrono::steady_clock::now();
if (j->first_pull) {
j->first_pull = 0;
j->last_pull_tp = now;
} else {
double gap_ms = std::chrono::duration<double, std::milli>(now - j->last_pull_tp).count();
j->last_pull_tp = now;
if (gap_ms > kStallGapMs) {
j->stalled = 1; /* сталь начался — просто флаг */
} else if (j->stalled) {
j->stalled = 0; /* сталь кончился */
int c;
{ std::lock_guard<std::mutex> lk(j->mtx); c = j->count; }
double depth_ms = (double)c * j->frame_ms
+ (double)j->st.numSamples() * 1000.0 / (double)j->sr;
if (depth_ms > kResyncDepthMs) vj_hard_resync(j);
}
}
}
/* 0) pre-roll: не отдаём звук, пока глубина кольца < target_ms (или не истёк таймаут).
* Состояние — только аудио-поток; кадры копятся в кольце продюсером (uasync). */
int cnt;
{
std::lock_guard<std::mutex> lk(j->mtx);
cnt = j->count;
}
if (j->pre_roll && !j->primed) {
if (cnt > 0 && !j->prime_started) {
j->prime_started = 1;
j->prime_t0 = std::chrono::steady_clock::now();
}
double ring_ms = (double)cnt * j->frame_ms;
double waited_ms = 0.0;
if (j->prime_started) {
auto now = std::chrono::steady_clock::now();
waited_ms = std::chrono::duration<double, std::milli>(now - j->prime_t0).count();
}
if (ring_ms >= j->target_ms || waited_ms >= kPrimeTimeoutMs) {
j->primed = 1;
} else {
return 0; /* тишина, ждём накопления */
}
}
/* 1) глубина (кольцо + внутренний буфер SoundTouch, сглаженная) → целевой темп (плавно) */
double depth_ms = (double)cnt * j->frame_ms + (double)j->st.numSamples() * 1000.0 / (double)j->sr;
j->ema_depth_ms = kEmaAlpha * depth_ms + (1.0 - kEmaAlpha) * j->ema_depth_ms;
double target = vj_compute_tempo(j->ema_depth_ms, j->target_ms, j->min_tempo, j->max_tempo);
double d = target - j->tempo;
if (d > kTempoStep) d = kTempoStep;
else if (d < -kTempoStep) d = -kTempoStep;
j->tempo += d;
j->st.setTempo(j->tempo);
j->tempo_x100.store((int)std::lround(j->tempo * 100.0));
/* 2) декодируем и кормим SoundTouch, пока не сможет выдать max_samples (или кольцо пусто).
* max_samples — суммарное число значений (интерлив); SoundTouch работает в слотах
* (слот = все каналы), поэтому max_slots = max_samples / channels. */
int ch = j->ch;
uint max_slots = (uint)max_samples / (uint)ch;
int guard = 0;
while (j->st.numSamples() < max_slots && guard++ < j->cap + 4) {
int elen = 0;
if (!vj_pop_enc(j, j->enc_buf, &elen)) break;
int n = j->decode(j->decode_arg, j->enc_buf, elen, j->pcm_buf.data(), j->frame_samples);
if (n <= 0) continue; /* битый/пустой кадр — пропускаем */
if (n > j->frame_samples) n = j->frame_samples;
for (int i = 0; i < n * ch; i++)
j->fin[(size_t)i] = (float)j->pcm_buf[(size_t)i] / 32768.0f;
j->st.putSamples(j->fin.data(), (uint)n);
}
/* 3) забираем готовое */
j->fout.resize((size_t)max_samples);
uint n = j->st.receiveSamples(j->fout.data(), max_slots);
for (uint i = 0; i < n * (uint)ch; i++) {
float v = j->fout[i] * 32767.0f;
if (v > 32767.0f) v = 32767.0f;
else if (v < -32768.0f) v = -32768.0f;
out[i] = (int16_t)std::lrintf(v);
}
if ((int)(n * (uint)ch) < max_samples) j->underruns++;
return (int)(n * (uint)ch);
}
void vj_end(struct vj* j) {
if (!j) return;
j->primed = 1; /* только аудио-поток: снять pre-roll и дренировать остаток */
}
void vj_get_stats(struct vj* j, int* depth_ms, int* tempo_x100, uint32_t* dropped, uint32_t* underruns) { if (!j) return;
if (depth_ms) {
/* полная глубина = кодированные кадры в кольце + невыданный PCM внутри SoundTouch */
std::lock_guard<std::mutex> lk(j->mtx);
double st_ms = (double)j->st.numSamples() * 1000.0 / (double)j->sr;
*depth_ms = (int)((double)j->count * j->frame_ms + st_ms + 0.5);
}
if (tempo_x100) *tempo_x100 = j->tempo_x100.load();
if (dropped) *dropped = j->dropped;
if (underruns) *underruns = j->underruns;
}
uint32_t vj_dropped(struct vj* j) {
if (!j) return 0;
std::lock_guard<std::mutex> lk(j->mtx);
return j->dropped;
}
uint32_t vj_resyncs(struct vj* j) {
if (!j) return 0;
std::lock_guard<std::mutex> lk(j->mtx);
return j->resyncs;
}
} // extern "C"
+79
View File
@@ -0,0 +1,79 @@
#ifndef VOICE_JITTER_H
#define VOICE_JITTER_H
// voice_jitter.h — адаптивный джиттер-буфер голоса с time-stretch (SoundTouch), C API.
//
// Хранит КОДИРОВАННЫЕ (Opus) кадры: uasync-поток кладёт кодированные кадры,
// аудио-поток на pull декодирует их (через коллбэк decode) и пропускает через
// SoundTouch с плавно меняющимся темпом. Излишек голоса держится до декодирования —
// декодируется только то, что готово к воспроизведению (экономия памяти ~48x и
// разгрузка uasync-потока).
//
// Потоки:
// - vj_push — uasync-поток (producer);
// - vj_pull — аудио-поток (consumer; единственный владелец decode/SoundTouch);
// - vj_get_stats — любой поток.
// Кольцо кодированных кадров защищено мьютексом (SPSC, дёшево: ~50 кадров/с).
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct vj;
/* decode: закодированный кадр → PCM. Возвращает число декодированных сэмплов. */
typedef int (*vj_decode_fn)(void* arg, const uint8_t* enc, int len, int16_t* pcm, int max_samples);
/* Полная конфигурация адаптивного джиттер-буфера. */
struct vj_cfg {
int sample_rate;
int channels;
int frame_samples; /* сэмплов в кадре (960 @20мс/48k) */
int frame_ms; /* длительность кадра в мс */
int max_frames; /* ёмкость кодированных кадров (50 = 1000мс) */
double target_ms; /* целевая глубина буфера */
double min_tempo; /* темп при пустом буфере (<1.0 = замедление, накопление) */
double max_tempo; /* темп при глубоком буфере (>1.0 = догон live) */
int pre_roll; /* 1 = не отдавать звук, пока глубина < target_ms */
vj_decode_fn decode;
void* decode_arg;
};
/* Создать буфер с полной конфигурацией (pre-roll, симметричный темп). */
struct vj* vj_create_cfg(const struct vj_cfg* cfg);
/* Создать буфер с конфигурацией по умолчанию (совместимо со звонком: без pre-roll,
* темп 1.0..1.6, target 60мс). frame_samples — сэмплов в кадре (960 @20мс/48k). */
struct vj* vj_create(int sample_rate, int channels, int frame_samples, int frame_ms,
int max_frames, vj_decode_fn decode, void* decode_arg);
void vj_destroy(struct vj* j);
/* producer: положить кодированный кадр (копируется). При переполнении дропает старейший. */
void vj_push(struct vj* j, const uint8_t* enc, int len);
/* consumer: выдать до max_samples декодированного+растянутого PCM. Может вернуть
* < max_samples при нехватке данных (андерфлоу). */
int vj_pull(struct vj* j, int16_t* out, int max_samples);
/* audio-поток: вход закончен (FIN) — принудительно снять pre-roll, чтобы дренировать
* накопленный остаток до конца. Безопасно вызывать многократно. */
void vj_end(struct vj* j);
/* Диагностика: глубина буфера (мс), темп (×100, 100 = 1.0x), дропнуто кадров
* при переполнении, число underrun-ов (pull вернул меньше запрошенного). */
void vj_get_stats(struct vj* j, int* depth_ms, int* tempo_x100, uint32_t* dropped, uint32_t* underruns);
/* Число дропнутых кадров при переполнении (для диагностики/тестов). */
uint32_t vj_dropped(struct vj* j);
/* Число хард-ресинков к live (после сталла аудио-девайса) — диагностика. */
uint32_t vj_resyncs(struct vj* j);
#ifdef __cplusplus
}
#endif
#endif /* VOICE_JITTER_H */
+53
View File
@@ -0,0 +1,53 @@
# voice_sources.cmake — голосовой стек звонка (единый для desktop/Android/headless).
#
# SoundTouch (time-stretch) + voice_jitter (кодированные кадры + decode-on-pull)
# + call_audio (Opus/тоны) + call_tones. C++ внутри, наружу — C-интерфейс
# (call_audio.h). Экспортирует UTUN_VOICE_SOURCES и UTUN_VOICE_INCLUDE_DIRS.
#
# Использование:
# set(LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../lib)
# set(CALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/call)
# include(${CALL_DIR}/voice_sources.cmake)
# utun_setup_voice("${LIB_DIR}" "${CALL_DIR}")
# add_library(utun_voice STATIC ${UTUN_VOICE_SOURCES})
# target_include_directories(utun_voice PUBLIC ${UTUN_VOICE_INCLUDE_DIRS})
# target_link_libraries(utun_voice PUBLIC opus) # opus_codec
function(utun_setup_voice LIB_DIR CALL_DIR)
set(_st_dir "${LIB_DIR}/soundtouch-master")
set(_src_dir "${CALL_DIR}/..")
set(UTUN_VOICE_SOURCES
${CALL_DIR}/call_audio.c
${CALL_DIR}/call_tones.c
${CALL_DIR}/voice_jitter.cpp
${_src_dir}/radio/radio_audio.c
${_st_dir}/source/SoundTouch/SoundTouch.cpp
${_st_dir}/source/SoundTouch/TDStretch.cpp
${_st_dir}/source/SoundTouch/RateTransposer.cpp
${_st_dir}/source/SoundTouch/FIFOSampleBuffer.cpp
${_st_dir}/source/SoundTouch/AAFilter.cpp
${_st_dir}/source/SoundTouch/FIRFilter.cpp
${_st_dir}/source/SoundTouch/InterpolateLinear.cpp
${_st_dir}/source/SoundTouch/InterpolateCubic.cpp
${_st_dir}/source/SoundTouch/InterpolateShannon.cpp
${_st_dir}/source/SoundTouch/cpu_detect_x86.cpp
${_st_dir}/source/SoundTouch/mmx_optimized.cpp
${_st_dir}/source/SoundTouch/sse_optimized.cpp
PARENT_SCOPE
)
set(UTUN_VOICE_INCLUDE_DIRS
${CALL_DIR}
${_src_dir}
${_src_dir}/transport_layer
${_src_dir}/routing_layer
${_src_dir}/chat
${_src_dir}/proxy
${_src_dir}/media_delivery
${_src_dir}/media_async
${LIB_DIR}
${LIB_DIR}/libopus/include
${_st_dir}/include
${_st_dir}/include/soundtouch
PARENT_SCOPE
)
endfunction()
+1 -5
View File
@@ -72,9 +72,7 @@ void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_i
}
if (si) {
si_register(inst, si, ch_id);
struct msg_insert_arg* ia = u_malloc(sizeof(*ia));
if (ia) { ia->inst = inst; snprintf(ia->ch_id, sizeof(ia->ch_id), "%s", ch_id); }
db_sync_set_insert_cb(si, on_msg_inserted, ia);
db_sync_set_insert_cb(si, on_msg_inserted, inst);
db_sync_instance_set_gated(si, 1);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: channel ready ch=%s tbl=%s group=0x%016llx",
CC_ID, ch_id, tbl_msg, (unsigned long long)gid);
@@ -137,8 +135,6 @@ void chat_core_create_channel(struct UTUN_INSTANCE* inst, struct chat_channel_cr
if (mrc < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member_sync_put(self) FAILED ch=%s rc=%d",
CC_ID, req->channel_id, mrc);
} else if (mrc & MS_APPLY_CHANGED) {
member_sync_broadcast_one(inst, req->channel_id, myid);
}
/* адреса + update_ts + подпись + синк — единой функцией */
chat_core_update_my_member(inst);
+131 -45
View File
@@ -15,6 +15,7 @@
#include "chat_setting.h"
#include "chat_member.h"
#include "member_sync.h"
#include "chat_sync.h"
#include "../utun_instance.h"
#include "../ntp_time.h"
@@ -79,6 +80,7 @@ void si_register(struct UTUN_INSTANCE* inst, struct DB_SYNC_INSTANCE* si, const
int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
if (!inst || !db_path) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid args inst=%p db_path=%p", CC_ID, (void*)inst, (void*)db_path); return -1; }
if (inst->chat_core) return 0;
struct chat_core_ctx* cc = u_calloc(1, sizeof(*cc));
if (!cc) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: OOM for chat_core_ctx", CC_ID); return -1; }
inst->chat_core = cc;
@@ -234,6 +236,7 @@ void chat_core_destroy(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized) return;
cc->initialized = 0;
member_sync_destroy(inst); /* Also releases callback lists after partial startup. */
if (cc->db && !cc->shared_db) { sqlite3_close(cc->db); }
inst->chat_core = NULL;
@@ -241,8 +244,8 @@ void chat_core_destroy(struct UTUN_INSTANCE* inst) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CC_ID);
}
sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst) { return CC(inst) ? CC(inst)->db : NULL; }
int chat_core_is_initialized(struct UTUN_INSTANCE* inst) { return CC(inst) ? CC(inst)->initialized : 0; }
sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst) { return (inst && CC(inst)) ? CC(inst)->db : NULL; }
int chat_core_is_initialized(struct UTUN_INSTANCE* inst) { return (inst && CC(inst)) ? CC(inst)->initialized : 0; }
/* ─── chat settings (GUI → uasync) ─── */
@@ -259,12 +262,60 @@ void chat_core_set_setting_trampoline(void* arg) {
u_free(arg);
}
/* ─── per-channel «play sound on new message» ─── */
int chat_core_get_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !ch_id[0]) return 1;
char key[128]; snprintf(key, sizeof(key), "ch_%s_sound_on_message", ch_id);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key=?", -1, &st, NULL) != SQLITE_OK) return 1;
sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC);
int on = 1;
if (sqlite3_step(st) == SQLITE_ROW) {
const char* v = (const char*)sqlite3_column_text(st, 0);
if (v) on = atoi(v) != 0;
}
sqlite3_finalize(st);
return on;
}
void chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !ch_id[0]) return;
char key[128]; snprintf(key, sizeof(key), "ch_%s_sound_on_message", ch_id);
char val[2]; snprintf(val, sizeof(val), "%d", on ? 1 : 0);
chat_core_save_ui_state(inst, key, val);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: sound_on_message ch=%s = %d", CC_ID, ch_id, on ? 1 : 0);
chat_sync_broadcast_sound_setting(inst, ch_id, on ? 1 : 0);
}
void chat_core_set_sound_on_message_trampoline(void* arg) {
struct chat_sound_arg* a = (struct chat_sound_arg*)arg;
if (!a) return;
chat_core_set_sound_on_message(a->inst, a->ch_id, a->on);
u_free(arg);
}
/* ─── headless API ─── */
static void json_escape(const char* src, char* dst, size_t dst_sz) {
size_t i = 0; const char* s = src;
while (*s && i < dst_sz - 2) {
if (*s == '"' || *s == '\\') { if (i + 1 < dst_sz - 2) dst[i++] = '\\'; }
const char* escape = NULL;
switch (*s) {
case '"': escape = "\\\""; break;
case '\\': escape = "\\\\"; break;
case '\n': escape = "\\n"; break;
case '\r': escape = "\\r"; break;
case '\t': escape = "\\t"; break;
case '\b': escape = "\\b"; break;
case '\f': escape = "\\f"; break;
}
if (escape) {
if (i + 2 >= dst_sz) break;
dst[i++] = escape[0]; dst[i++] = escape[1]; s++; continue;
}
dst[i++] = *s++;
}
dst[i] = '\0';
@@ -367,7 +418,8 @@ int chat_core_get_messages_json(struct UTUN_INSTANCE* inst, const char* ch_id, i
int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !buf || !out_len) return -1;
if (!cc || !cc->initialized || !ch_id || !buf || !out_len || buf_size < 3) return -1;
*out_len = 0;
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
char* w = buf; char* end = buf + buf_size;
*w++ = '['; const char* sep = "";
@@ -376,6 +428,12 @@ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, ch
"SELECT p.node_id, n.name, n.x25519_pubkey, n.ed25519_pubkey, p.adm_tags "
"FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id", peers_tbl);
sqlite3_stmt* st = NULL;
sqlite3_stmt* as = NULL;
#define MEMBERS_APPEND(...) do { \
int written = snprintf(w, (size_t)(end - w), __VA_ARGS__); \
if (written < 0 || (size_t)written >= (size_t)(end - w)) goto overflow; \
w += written; \
} while (0)
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; }
while (sqlite3_step(st) == SQLITE_ROW) {
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
@@ -386,7 +444,7 @@ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, ch
int x25519_len = sqlite3_column_bytes(st, 2);
int ed25519_len = sqlite3_column_bytes(st, 3);
const char* adm_tags = (const char*)sqlite3_column_text(st, 4);
char esc_name[256], x25519_hex[65], ed25519_hex[65], esc_tags[384];
char esc_name[256], x25519_hex[65], ed25519_hex[65], esc_tags[6 * MS_ADM_TAGS_MAX + 1];
json_escape(name ? name : "", esc_name, sizeof(esc_name));
x25519_hex[0] = ed25519_hex[0] = '\0';
if (x25519 && x25519_len >= 32) for (int i = 0; i < 32; i++) snprintf(x25519_hex + i*2, 3, "%02x", x25519[i]);
@@ -401,14 +459,13 @@ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, ch
json_escape(adm_tags ? adm_tags : "", esc_tags, sizeof(esc_tags));
w += snprintf(w, (size_t)(end - w),
MEMBERS_APPEND(
"%s{\"node_id\":\"0x%016llx\",\"name\":\"%s\",\"online\":%d,\"connected\":%d,"
"\"adm_tags\":\"%s\",\"x25519\":\"%s\",\"ed25519\":\"%s\",\"addrs\":[",
sep, (unsigned long long)nid, esc_name, online, connected, esc_tags, x25519_hex, ed25519_hex);
sep = ",";
/* addresses */
sqlite3_stmt* as = NULL;
char asql[200]; snprintf(asql, sizeof(asql),
"SELECT family, protocol, address, port, rtt FROM node_addresses WHERE node_id=? ORDER BY family, protocol");
if (sqlite3_prepare_v2(cc->db, asql, -1, &as, NULL) == SQLITE_OK) {
@@ -429,20 +486,24 @@ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, ch
addr[0],addr[1],addr[2],addr[3],addr[4],addr[5],addr[6],addr[7],
addr[8],addr[9],addr[10],addr[11],addr[12],addr[13],addr[14],addr[15]);
else { snprintf(ip_str, sizeof(ip_str), "?"); }
w += snprintf(w, (size_t)(end - w),
MEMBERS_APPEND(
"%s{\"ip\":\"%s\",\"port\":%d,\"proto\":\"%s\",\"rtt\":%d}",
asep2, ip_str, port, proto == 1 ? "UDP" : proto == 2 ? "TCP" : "?", rtt);
asep2 = ",";
}
sqlite3_finalize(as);
sqlite3_finalize(as); as = NULL;
}
w += snprintf(w, (size_t)(end - w), "]}");
MEMBERS_APPEND("]}");
}
MEMBERS_APPEND("]");
sqlite3_finalize(st);
*w++ = ']';
*w = '\0';
*out_len = (size_t)(w - buf);
return 0;
overflow:
sqlite3_finalize(as); sqlite3_finalize(st); buf[0] = 0;
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: members JSON exceeds output buffer bytes=%zu", CC_ID, buf_size);
return -1;
#undef MEMBERS_APPEND
}
int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz) {
@@ -464,7 +525,7 @@ int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char*
struct chat_member_tags_key {
char key[CHAT_MEMBER_TAGS_KEY_SZ];
char val[CHAT_MEMBER_TAGS_VAL_SZ];
char* val;
char* raw; /* malloc'd: serialized append-only string (admin/moder) or NULL (= val is raw) */
};
@@ -494,20 +555,19 @@ static int tags_is_append_key(const char* key) {
return strcmp(key, "admin") == 0 || strcmp(key, "moder") == 0;
}
static void tags_set_effective_val(struct chat_member_tags_key* k, const char* raw) {
uint64_t ts = 0;
const char* eff = tags_append_effective(raw, &ts);
snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", eff);
(void)ts;
}
static int tags_json_cb(const char* key, const char* json_val, void* arg) {
struct chat_member_tags* t = (struct chat_member_tags*)arg;
if (t->key_count >= CHAT_MEMBER_TAGS_MAX_KEYS) return -1;
if (t->key_count >= CHAT_MEMBER_TAGS_MAX_KEYS || strlen(key) >= CHAT_MEMBER_TAGS_KEY_SZ) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags JSON key limit exceeded", CC_ID); return -1;
}
struct chat_member_tags_key* k = &t->keys[t->key_count];
snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key);
snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", json_val);
k->val = u_strdup(tags_is_append_key(key) ? tags_append_effective(json_val, NULL) : json_val);
k->raw = tags_is_append_key(key) ? u_strdup(json_val) : NULL;
if (!k->val || (tags_is_append_key(key) && !k->raw)) {
u_free(k->val); u_free(k->raw); memset(k, 0, sizeof(*k));
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags JSON allocation failed", CC_ID); return -1;
}
t->key_count++;
return 0;
}
@@ -527,7 +587,9 @@ struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
const char* tags = (const char*)sqlite3_column_text(st, 0);
if (tags && tags[0]) chat_member_tags_replace(t, tags);
if (tags && tags[0] && chat_member_tags_replace(t, tags) < 0) {
sqlite3_finalize(st); chat_member_tags_free(t); return NULL;
}
}
sqlite3_finalize(st);
}
@@ -539,22 +601,24 @@ struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const
int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val) {
if (!t || !key) return -1;
if (strlen(key) >= CHAT_MEMBER_TAGS_KEY_SZ) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — key too long", CC_ID); return -1; }
if (val && strlen(val) >= CHAT_MEMBER_TAGS_VAL_SZ) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — val too long", CC_ID); return -1; }
if (val && strlen(val) > MS_ADM_TAGS_MAX) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — val too long", CC_ID); return -1; }
int idx = tags_find(t, key);
if (!val) {
if (idx >= 0) { u_free(t->keys[idx].raw); t->keys[idx] = t->keys[--t->key_count]; }
if (idx >= 0) { u_free(t->keys[idx].raw); u_free(t->keys[idx].val); t->keys[idx] = t->keys[--t->key_count]; }
return 0;
}
char* copy = u_strdup(val);
if (!copy) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set allocation failed", CC_ID); return -1; }
if (idx >= 0) {
snprintf(t->keys[idx].val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", val);
u_free(t->keys[idx].val); t->keys[idx].val = copy;
} else if (t->key_count < CHAT_MEMBER_TAGS_MAX_KEYS) {
struct chat_member_tags_key* k = &t->keys[t->key_count++];
snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key);
snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", val);
k->val = copy;
k->raw = NULL;
} else {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — max keys reached", CC_ID); return -1;
u_free(copy); DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — max keys reached", CC_ID); return -1;
}
return 0;
}
@@ -566,11 +630,17 @@ const char* chat_member_tags_get(struct chat_member_tags* t, const char* key) {
}
int chat_member_tags_replace(struct chat_member_tags* t, const char* json) {
if (!t || !json) return -1;
for (int i = 0; i < t->key_count; i++) u_free(t->keys[i].raw);
t->key_count = 0;
int rc = json_flat_parse(json, tags_json_cb, t);
if (rc != 0) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace — invalid JSON", CC_ID); return -1; }
if (!t || !json || strlen(json) > MS_ADM_TAGS_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace invalid input or size", CC_ID); return -1;
}
struct chat_member_tags parsed = {0};
int rc = json_flat_parse(json, tags_json_cb, &parsed);
if (rc != 0) {
for (int i = 0; i < parsed.key_count; i++) { u_free(parsed.keys[i].raw); u_free(parsed.keys[i].val); }
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace — invalid JSON", CC_ID); return -1;
}
for (int i = 0; i < t->key_count; i++) { u_free(t->keys[i].raw); u_free(t->keys[i].val); }
t->key_count = parsed.key_count; memcpy(t->keys, parsed.keys, sizeof(t->keys));
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace ch=%s nid=0x%016llx keys=%d",
CC_ID, t->ch_id, (unsigned long long)t->node_id, t->key_count);
return 0;
@@ -581,8 +651,14 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags*
if (!t || !cc || !cc->initialized || !cc->inst || !cc->db) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — invalid state", CC_ID); return -1;
}
char json[4096]; int off = 0, ver = 1;
#define TAGS_ADVANCE(n) do { if (off < (int)sizeof(json) - 10) off += (int)(n); else off = (int)sizeof(json); } while(0)
char json[MS_ADM_TAGS_MAX + 1]; int off = 0, ver = 1;
#define TAGS_ADVANCE(expr) do { \
int written = (expr); \
if (written < 0 || (size_t)written >= sizeof(json) - (size_t)off) { \
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit JSON exceeds %d bytes", CC_ID, MS_ADM_TAGS_MAX); return -1; \
} \
off += written; \
} while (0)
int vi = tags_find(t, "ver");
if (vi >= 0) ver = atoi(t->keys[vi].val) + 1;
TAGS_ADVANCE(snprintf(json, sizeof(json), "{\"ver\":\"%d\"", ver));
@@ -611,7 +687,13 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags*
} else {
out_val = k->val;
}
TAGS_ADVANCE(snprintf(json + off, sizeof(json) - (size_t)off, ",\"%s\":\"%s\"", k->key, out_val));
char escaped_key[6 * CHAT_MEMBER_TAGS_KEY_SZ + 1], escaped_value[6 * MS_ADM_TAGS_MAX + 1];
if (strlen(out_val) > MS_ADM_TAGS_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit value exceeds limit", CC_ID); return -1;
}
json_escape(k->key, escaped_key, sizeof(escaped_key));
json_escape(out_val, escaped_value, sizeof(escaped_value));
TAGS_ADVANCE(snprintf(json + off, sizeof(json) - (size_t)off, ",\"%s\":\"%s\"", escaped_key, escaped_value));
}
TAGS_ADVANCE(snprintf(json + off, sizeof(json) - (size_t)off, "}"));
#undef TAGS_ADVANCE
@@ -620,10 +702,9 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags*
if (topo_node_sqlite_channel_get_priv(cc->db, t->ch_id, ch_ed_priv) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — no channel privkey for ch=%s (not owner?)", CC_ID, t->ch_id); return -1;
}
uint8_t sign_msg[8192]; size_t mlen = (size_t)off;
if (mlen + 8 > sizeof(sign_msg)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — json too big", CC_ID); return -1; }
memcpy(sign_msg, json, mlen);
memcpy(sign_msg + mlen, &t->node_id, 8); mlen += 8;
uint8_t sign_msg[MS_ADM_TAGS_MAX + 8];
int mlen = member_sync_build_owner_msg(t->node_id, json, sign_msg, sizeof(sign_msg));
if (mlen < 0) return -1;
uint8_t sig[64];
if (sc_ed25519_sign(ch_ed_priv, sign_msg, mlen, sig) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — sign failed", CC_ID); return -1;
@@ -632,7 +713,7 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags*
char peers_tbl[80]; peers_table_name(t->ch_id, peers_tbl, sizeof(peers_tbl));
sqlite3_stmt* st = NULL;
char sql[384]; snprintf(sql, sizeof(sql),
"SELECT x25519_pubkey, ed25519_pubkey, join_sig, join_ts, update_sig, update_ts, userinfo, storage"
"SELECT x25519_pubkey, ed25519_pubkey, join_sig, join_ts, update_sig, update_ts, userinfo, storage, source"
" FROM \"%s\" WHERE node_id=?", peers_tbl);
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member query failed", CC_ID); return -1;
@@ -649,9 +730,15 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags*
uint64_t uts = (uint64_t)sqlite3_column_int64(st, 5);
const char* uinfo = (const char*)sqlite3_column_text(st, 6);
int stg = sqlite3_column_int(st, 7);
int src = sqlite3_column_int(st, 8);
if (!x25 || !ed) { sqlite3_finalize(st);
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member has no keys ch=%s", CC_ID, t->ch_id); return -1;
}
if (src != 0) { sqlite3_finalize(st);
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — cannot set flags on placeholder member (source=%d) ch=%s nid=0x%016llx",
CC_ID, src, t->ch_id, (unsigned long long)t->node_id);
return -1;
}
const char* stg_val = chat_member_tags_get(t, "storage");
if (stg_val && strcmp(stg_val, "yes") == 0) stg = 1;
@@ -659,16 +746,15 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags*
uinfo ? uinfo : "", json, sig, stg, 0, NULL);
sqlite3_finalize(st);
if (rc < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member_sync_put failed rc=%d", CC_ID, rc); return -1; }
if (rc & MS_APPLY_CHANGED) member_sync_broadcast_one(inst, t->ch_id, t->node_id);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit ch=%s nid=0x%016llx ver=%d keys=%d json=%s",
CC_ID, t->ch_id, (unsigned long long)t->node_id, ver, t->key_count, json);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit ch=%s nid=0x%016llx ver=%d keys=%d bytes=%d",
CC_ID, t->ch_id, (unsigned long long)t->node_id, ver, t->key_count, off);
return 0;
}
void chat_member_tags_free(struct chat_member_tags* t) {
if (!t) return;
for (int i = 0; i < t->key_count; i++) u_free(t->keys[i].raw);
for (int i = 0; i < t->key_count; i++) { u_free(t->keys[i].raw); u_free(t->keys[i].val); }
u_free(t);
}
+52 -16
View File
@@ -1,5 +1,7 @@
/*
* chat_core.h — центральный API чата в потоке uasync
* chat_core — API каналов, сообщений, профиля и настроек одного узла.
* chat_service_start/stop управляют всем чат-сервисом поверх работающего ядра.
* Данные хранятся в общей SQLite; события для интерфейса доставляет chat_event.
*
* Все DB-операции и сетевые функции выполняются в uasync-потоке.
* Все функции per-instance: первым аргументом struct UTUN_INSTANCE*.
@@ -19,7 +21,16 @@ 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: регистрирует уже скачанные медиафайлы (лежат на диске, но отсутствуют
@@ -31,33 +42,36 @@ void chat_media_backfill(struct UTUN_INSTANCE* inst);
void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst);
/* Регистрирует триггер: после первого SYNC_DONE (+ дебаунс 2с) анонсирует локальные
* блоки суперузлам и запускает chat_media_autodownload_backfill. Один раз за процесс. */
* блоки суперузлам и запускает 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];
char content_type[32];
char media_src[1024];
char media_dest[1024];
uint8_t media_copy;
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;
uint32_t data_len;
uint64_t timestamp;
uint8_t* data; /* содержимое сообщения, заимствовано обычным submit */
uint32_t data_len; /* размер data в байтах */
uint64_t timestamp; /* входное время; отправка назначает новый timestamp через db_sync */
uint64_t reply_to_ts; /* 0 = не ответ */
uint64_t reply_to_node_id; /* node_id автора исходного сообщения */
};
/* Подписать, сохранить и разослать сообщение. req/data остаются у вызывающего. */
void chat_core_submit_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
/* Отправка медиа-сообщения (вложение): src->dest (copy) или видео (transcode). */
@@ -65,10 +79,18 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
/* ── 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) ── */
@@ -87,9 +109,12 @@ 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 остаётся у вызывающего. */
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);
@@ -108,18 +133,30 @@ 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);
/* Единое обновление своего мембера: адреса + bump update_ts + подпись (с адресами) + синк */
/* Обновить своё 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_<id>_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[256]; };
void chat_core_save_ui_state_trampoline(void* arg);
@@ -190,13 +227,13 @@ void chat_core_collect_member_detail_trampoline(void* arg);
/* Список каналов с метаданными в 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}]
* count=0 → все, offset=0 → сначала */
/* Сообщения канала в 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,x25519,ed25519,addrs:[{ip,port,proto,rtt}]}] */
/* Мемберы в 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 */
@@ -206,7 +243,6 @@ int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char*
#define CHAT_MEMBER_TAGS_MAX_KEYS 16
#define CHAT_MEMBER_TAGS_KEY_SZ 64
#define CHAT_MEMBER_TAGS_VAL_SZ 256
struct chat_member_tags;
+4 -2
View File
@@ -92,6 +92,9 @@ struct chat_core_ctx {
void* media_backfill_timer;/* was chat_msg.c g_media_startup_backfill_timer */
int media_backfill_ran; /* was chat_msg.c g_media_startup_backfill_ran */
struct UTUN_INSTANCE* media_backfill_inst; /* was chat_msg.c g_media_startup_backfill_inst */
void* media_channel_backfill_timer; /* debounce per-channel докачки */
char media_channel_backfill[16][64]; /* pending ch_id */
int media_channel_backfill_pending;
};
/* Per-instance chat context. inst->chat_core обязателен после chat_core_init. */
@@ -110,8 +113,7 @@ void si_register(struct UTUN_INSTANCE* inst, struct DB_SYNC_INSTANCE* si, const
/* ── db_sync callback (chat_msg.c) ── */
struct msg_insert_arg { struct UTUN_INSTANCE* inst; char ch_id[64]; };
void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, void* arg);
void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, int initial_sync, void* arg);
/* ── применить настройку group_autoconnect ко всем CHAT-группам (chat_channel.c) ── */
+8 -1
View File
@@ -39,8 +39,15 @@ void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data,
[22]="MEMBER_DETAIL", [23]="MEMBER_LIST",
[24]="MEMBER_UPDATED", [25]="MEMBER_REMOVED",
[26]="BGP_INFO", [27]="CONNECTING_NODES",
[28]="ADMIN_KEY_RECEIVED", [29]="INVITE_LINK_READY",
[30]="INVITE_CANDIDATES", [31]="CHANNEL_DELETED",
[32]="DM_MSG_RECEIVED", [33]="DM_CONV_UPDATED",
[34]="CALL_INCOMING", [35]="CALL_RINGING", [36]="CALL_ACCEPTED",
[37]="CALL_ENDED", [38]="CALL_DECLINED", [39]="CALL_STATS",
[40]="CALL_ERROR", [41]="DM_MESSAGES", [42]="CALL_PATH",
[43]="RADIO_TALK",
};
const char* n = (type >= 1 && type <= 27) ? names[type] : "?";
const char* n = (type >= 1 && type <= 43) ? names[type] : "?";
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "chat_event: %s(%d) data=%d bytes", n, type, len);
if (data && len > 0) log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_CHAT, " ", data, len);
}
+12 -2
View File
@@ -36,7 +36,7 @@ extern "C" {
#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] */
#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},...] */
@@ -49,11 +49,21 @@ extern "C" {
#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][link_len:2][link:var] — пустой link = ошибка */
#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] — беседа создана/изменена */
#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_ms:2][buffer_ms:2][tempo_x100: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][count:2][msg:var]* — расшифрованные сообщения беседы */
#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] — кто-то начал/закончил говорить */
typedef void (*chat_event_handler_fn)(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
+179 -31
View File
@@ -19,6 +19,9 @@
#include "invite_build.h"
#include "../dm/dm_core.h"
#include "../dm/dm_crypto.h"
#include "../call/call.h"
#include "../call/call_proto.h"
#include "../radio/radio.h"
#include "../transport_layer/secure_channel.h"
#include "../transport_layer/etcp.h"
#include "../transport_layer/etcp_connections.h"
@@ -61,10 +64,18 @@ struct headless_client {
int send_offset;
int subscribed;
int closing;
struct headless_invite* invites;
struct headless_control* hc;
struct headless_client* next;
};
struct headless_invite {
struct headless_invite* next;
struct headless_client* client;
struct chat_invite_build* pending;
int id;
};
struct headless_control {
struct UASYNC* ua;
struct UTUN_INSTANCE* inst;
@@ -73,6 +84,7 @@ struct headless_control {
socket_t listen_fd;
void* listen_sock_id;
int running;
void* cleanup_timer;
};
static struct headless_control* hc_of(struct UTUN_INSTANCE* inst) {
@@ -82,6 +94,7 @@ static struct headless_control* hc_of(struct UTUN_INSTANCE* inst) {
/* ── Forward declarations ── */
static void hc_handle_command(struct headless_client* cli, const char* json);
static void hc_close_client(struct headless_client* cli);
/* ── JSON helpers ── */
@@ -143,6 +156,10 @@ static void cli_send(struct headless_client* cli, const char* data, size_t len)
if (remain <= sizeof(cli->send_buf)) { memcpy(cli->send_buf, data + r, remain); cli->send_len = (int)remain; cli->send_offset = 0; }
if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 1);
}
if (r < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: send failed err=%d, closing client", socket_get_error());
hc_close_client(cli);
}
}
static void cli_send_str(struct headless_client* cli, const char* s) {
@@ -201,6 +218,27 @@ static void hc_on_chat_event(struct UTUN_INSTANCE* inst, int type, const uint8_t
} else if (type == 33 && data && len >= 2) { /* DM_CONV_UPDATED */
uint8_t cl = data[0];
snprintf(json, sizeof(json), "{\"event\":\"dm_conv\",\"conv\":\"%.*s\"}", (int)cl, data + 1);
} else if (type == CHAT_EVT_CALL_INCOMING && data && len >= 1 + 8 + 8) { /* CALL_INCOMING */
uint8_t cl = data[0];
if (len >= 1 + (int)cl + 8 + 8) {
uint64_t caller; memcpy(&caller, data + 1 + cl, 8);
uint64_t cid; memcpy(&cid, data + 1 + cl + 8, 8);
snprintf(json, sizeof(json),
"{\"event\":\"call_incoming\",\"ch\":\"%.*s\",\"caller_id\":\"0x%016llx\",\"call_id\":\"0x%016llx\"}",
(int)cl, data + 1, (unsigned long long)caller, (unsigned long long)cid);
}
} else if (type == CHAT_EVT_CALL_RINGING && data && len >= 8) { /* CALL_RINGING */
uint64_t cid; memcpy(&cid, data, 8);
snprintf(json, sizeof(json), "{\"event\":\"call_ringing\",\"call_id\":\"0x%016llx\"}", (unsigned long long)cid);
} else if (type == CHAT_EVT_CALL_ACCEPTED && data && len >= 8) { /* CALL_ACCEPTED */
uint64_t cid; memcpy(&cid, data, 8);
snprintf(json, sizeof(json), "{\"event\":\"call_accepted\",\"call_id\":\"0x%016llx\"}", (unsigned long long)cid);
} else if (type == CHAT_EVT_CALL_ENDED && data && len >= 9) { /* CALL_ENDED */
uint64_t cid; memcpy(&cid, data, 8);
snprintf(json, sizeof(json), "{\"event\":\"call_ended\",\"call_id\":\"0x%016llx\",\"reason\":%d}", (unsigned long long)cid, data[8]);
} else if (type == CHAT_EVT_CALL_DECLINED && data && len >= 9) { /* CALL_DECLINED */
uint64_t cid; memcpy(&cid, data, 8);
snprintf(json, sizeof(json), "{\"event\":\"call_declined\",\"call_id\":\"0x%016llx\",\"reason\":%d}", (unsigned long long)cid, data[8]);
}
if (json[0]) hc_broadcast_event(hc, json);
}
@@ -214,7 +252,11 @@ static void client_write_callback(socket_t fd, void* arg) {
if (cli->send_len <= 0) { if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 0); return; }
ssize_t r = send(cli->fd, cli->send_buf + cli->send_offset, (int)(cli->send_len - cli->send_offset), 0);
if (r < 0 && (socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK)) return;
if (r <= 0) { cli->send_len = 0; cli->send_offset = 0; if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 0); return; }
if (r <= 0) {
DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: send failed (r=%zd err=%d) — closing client", r, socket_get_error());
hc_close_client(cli);
return;
}
cli->send_offset += (int)r;
if (cli->send_offset >= cli->send_len) { cli->send_len = 0; cli->send_offset = 0; if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 0); }
}
@@ -225,7 +267,9 @@ static void hc_close_client(struct headless_client* cli) {
struct headless_control* hc = cli ? cli->hc : NULL;
if (!cli || cli->closing) return;
cli->closing = 1;
while (cli->invites) chat_invite_build_cancel(cli->invites->pending);
if (hc && hc->ua && cli->socket_id) uasync_remove_socket_t(hc->ua, cli->fd);
cli->socket_id = NULL;
if (cli->fd != SOCKET_INVALID) { socket_close_wrapper(cli->fd); cli->fd = SOCKET_INVALID; }
}
@@ -240,6 +284,7 @@ static void client_read_callback(socket_t fd, void* arg) {
for (ssize_t i = 0; i < r; i++) {
if (tmp[i] == '\n') {
if (cli->recv_len > 0) { cli->recv_buf[cli->recv_len] = '\0'; hc_handle_command(cli, cli->recv_buf); cli->recv_len = 0; }
if (cli->closing) return;
} else if (cli->recv_len < RECV_BUF_SIZE - 1) {
cli->recv_buf[cli->recv_len++] = tmp[i];
}
@@ -415,6 +460,34 @@ static void hc_handle_attach(struct headless_client* cli, int id, const char* js
send_response(cli, id, resp, NULL);
}
static void hc_invite_ready(void* arg, int result, const char* link) {
struct headless_invite* req = arg;
struct headless_client* cli = req->client;
struct headless_invite** p = &cli->invites;
while (*p && *p != req) p = &(*p)->next;
if (*p) *p = req->next;
int id = req->id;
u_free(req);
if (cli->closing) return;
if (result != CHAT_JOIN_OK) {
send_response(cli, id, NULL, result == CHAT_JOIN_TIMEOUT ? "invite registration timed out" : "invite registration failed");
return;
}
struct InviteData d; char err[128];
if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: cannot decode confirmed invite: %s", err);
send_response(cli, id, NULL, "failed to decode built link");
return;
}
DEBUG_INFO(DEBUG_CATEGORY_HEADLESS, "headless: invite confirmed ch=%llu node=%016llx addrs=%d",
(unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
char resp[1280];
snprintf(resp, sizeof(resp),
"{\"link\":\"%s\",\"channel_id\":%llu,\"node_id\":\"0x%016llx\",\"addrs\":%d}",
link, (unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
send_response(cli, id, resp, NULL);
}
static void hc_handle_invite(struct headless_client* cli, int id, const char* json) {
struct headless_control* hc = cli->hc;
char ch[64], node_id_str[32];
@@ -429,26 +502,16 @@ static void hc_handle_invite(struct headless_client* cli, int id, const char* js
if (json_get_str(json, "node_id", node_id_str, sizeof(node_id_str)) == 0)
target_node_id = strtoull(node_id_str, NULL, 16);
char link[1024];
if (chat_invite_build_link(hc->inst, channel_id, target_node_id, NULL, link, sizeof(link)) < 0) {
send_response(cli, id, NULL, "failed to build invite link (see log)");
return;
struct headless_invite* req = u_calloc(1, sizeof(*req));
if (!req) {
DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: invite allocation failed");
send_response(cli, id, NULL, "out of memory"); return;
}
struct InviteData d; char err[128];
if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) {
send_response(cli, id, NULL, "failed to decode built link");
return;
}
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: invite ch=%s target=0x%016llx node=0x%016llx addrs=%d",
ch, (unsigned long long)target_node_id, (unsigned long long)d.nodeId, (int)d.addrCount);
char resp[1280];
snprintf(resp, sizeof(resp),
"{\"link\":\"%s\",\"channel_id\":%llu,\"node_id\":\"0x%016llx\",\"addrs\":%d}",
link, (unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
send_response(cli, id, resp, NULL);
req->client = cli; req->id = id;
req->pending = chat_invite_build_link(hc->inst, channel_id, target_node_id, NULL, hc_invite_ready, req);
if (!req->pending) { u_free(req); send_response(cli, id, NULL, "failed to build invite link (see log)"); return; }
req->next = cli->invites;
cli->invites = req;
}
static void hc_handle_invite_nodes(struct headless_client* cli, int id, const char* json) {
@@ -650,15 +713,92 @@ static void hc_handle_dm_send(struct headless_client* cli, int id, const char* j
send_response(cli, id, "{\"sent\":true}", NULL);
}
/* ── Звонки (P2P аудио) ── */
static uint64_t hc_gen_call_id(void) {
uint64_t id = ((uint64_t)rand() << 32) ^ (uint64_t)rand();
return id ? id : 1;
}
static void hc_handle_call_start(struct headless_client* cli, int id, const char* json) {
char ch[64], node_id_str[32], call_id_str[32];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
if (json_get_str(json, "node_id", node_id_str, sizeof(node_id_str)) < 0) { send_response(cli, id, NULL, "missing 'node_id' param"); return; }
uint64_t peer = strtoull(node_id_str, NULL, 16);
if (!peer || !cli->hc->inst) { send_response(cli, id, NULL, "invalid node_id"); return; }
uint64_t call_id = 0;
if (json_get_str(json, "call_id", call_id_str, sizeof(call_id_str)) == 0) call_id = strtoull(call_id_str, NULL, 16);
if (!call_id) call_id = hc_gen_call_id();
call_start(cli->hc->inst, ch, peer, call_id);
char resp[256]; snprintf(resp, sizeof(resp), "{\"call_id\":\"0x%016llx\",\"calling\":true}", (unsigned long long)call_id);
send_response(cli, id, resp, NULL);
}
static void hc_handle_call_accept(struct headless_client* cli, int id, const char* json) {
char call_id_str[32];
if (json_get_str(json, "call_id", call_id_str, sizeof(call_id_str)) < 0) { send_response(cli, id, NULL, "missing 'call_id' param"); return; }
uint64_t call_id = strtoull(call_id_str, NULL, 16);
call_accept(cli->hc->inst, call_id);
send_response(cli, id, "{\"accepted\":true}", NULL);
}
static void hc_handle_call_decline(struct headless_client* cli, int id, const char* json) {
char call_id_str[32];
if (json_get_str(json, "call_id", call_id_str, sizeof(call_id_str)) < 0) { send_response(cli, id, NULL, "missing 'call_id' param"); return; }
uint64_t call_id = strtoull(call_id_str, NULL, 16);
call_decline(cli->hc->inst, call_id);
send_response(cli, id, "{\"declined\":true}", NULL);
}
static void hc_handle_call_hangup(struct headless_client* cli, int id, const char* json) {
char call_id_str[32];
if (json_get_str(json, "call_id", call_id_str, sizeof(call_id_str)) < 0) { send_response(cli, id, NULL, "missing 'call_id' param"); return; }
uint64_t call_id = strtoull(call_id_str, NULL, 16);
call_hangup(cli->hc->inst, call_id);
send_response(cli, id, "{\"hungup\":true}", NULL);
}
/* ── Рация (PTT walkie-talkie) ── */
static void hc_handle_radio_on(struct headless_client* cli, int id, const char* json) {
char ch[64];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
uint64_t group_id = strtoull(ch, NULL, 10);
if (!group_id || !cli->hc->inst) { send_response(cli, id, NULL, "invalid ch"); return; }
radio_set_active(cli->hc->inst, group_id, 1);
char resp[128]; snprintf(resp, sizeof(resp), "{\"radio\":true,\"ch\":\"%llu\"}", (unsigned long long)group_id);
send_response(cli, id, resp, NULL);
}
static void hc_handle_radio_off(struct headless_client* cli, int id, const char* json) {
char ch[64];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
uint64_t group_id = strtoull(ch, NULL, 10);
if (!group_id || !cli->hc->inst) { send_response(cli, id, NULL, "invalid ch"); return; }
radio_set_active(cli->hc->inst, group_id, 0);
char resp[128]; snprintf(resp, sizeof(resp), "{\"radio\":false,\"ch\":\"%llu\"}", (unsigned long long)group_id);
send_response(cli, id, resp, NULL);
}
static void hc_handle_radio_subscribers(struct headless_client* cli, int id, const char* json) {
char ch[64];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
uint64_t group_id = strtoull(ch, NULL, 10);
int n = radio_subscribers(cli->hc->inst, group_id);
char resp[128]; snprintf(resp, sizeof(resp), "{\"subscribers\":%d,\"ch\":\"%llu\"}", n, (unsigned long long)group_id);
send_response(cli, id, resp, NULL);
}
/* ── Command dispatch ── */
typedef void (*hc_cmd_fn)(struct headless_client* cli, int id, const char* json);
static const char* cmd_names[] = { "ping", "status", "channels", "members", "messages", "send", "attach", "invite", "invite_nodes", "connect", "create_channel", "invite_to", "subscribe", "dm_start", "dm_list", "dm_messages", "dm_send", "quit", NULL };
static hc_cmd_fn cmd_handlers[] = { hc_handle_ping, hc_handle_status, hc_handle_channels, hc_handle_members, hc_handle_messages, hc_handle_send, hc_handle_attach, hc_handle_invite, hc_handle_invite_nodes, hc_handle_connect, hc_handle_create_channel, hc_handle_invite_to, hc_handle_subscribe, hc_handle_dm_start, hc_handle_dm_list, hc_handle_dm_messages, hc_handle_dm_send, hc_handle_quit };
static const char* cmd_names[] = { "ping", "status", "channels", "members", "messages", "send", "attach", "invite", "invite_nodes", "connect", "create_channel", "invite_to", "subscribe", "dm_start", "dm_list", "dm_messages", "dm_send", "call_start", "call_accept", "call_decline", "call_hangup", "radio_on", "radio_off", "radio_subscribers", "quit", NULL };
static hc_cmd_fn cmd_handlers[] = { hc_handle_ping, hc_handle_status, hc_handle_channels, hc_handle_members, hc_handle_messages, hc_handle_send, hc_handle_attach, hc_handle_invite, hc_handle_invite_nodes, hc_handle_connect, hc_handle_create_channel, hc_handle_invite_to, hc_handle_subscribe, hc_handle_dm_start, hc_handle_dm_list, hc_handle_dm_messages, hc_handle_dm_send, hc_handle_call_start, hc_handle_call_accept, hc_handle_call_decline, hc_handle_call_hangup, hc_handle_radio_on, hc_handle_radio_off, hc_handle_radio_subscribers, hc_handle_quit };
static void hc_handle_command(struct headless_client* cli, const char* json) {
if (!cli || !json) return;
if (!cli || cli->closing || !json) return;
int id = json_get_int(json, "id", 0);
const char* cs = json_get_cmd(json);
if (!cs) { send_response(cli, id, NULL, "missing cmd"); return; }
@@ -689,7 +829,7 @@ static void accept_callback(socket_t fd, void* arg) {
cli->fd = cfd;
cli->hc = hc;
cli->socket_id = uasync_add_socket_t(hc->ua, cfd, client_read_callback, client_write_callback, NULL, cli);
cli->socket_id = uasync_add_socket_t(hc->ua, cfd, client_read_callback, client_write_callback, NULL, "chatctl_client", cli);
if (!cli->socket_id) { u_free(cli); socket_close_wrapper(cfd); DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: failed to register client fd"); return; }
cli->next = hc->clients; hc->clients = cli; hc->client_count++;
@@ -700,6 +840,7 @@ static void accept_callback(socket_t fd, void* arg) {
static void hc_cleanup(void* arg) {
struct headless_control* hc = (struct headless_control*)arg;
hc->cleanup_timer = NULL;
struct headless_client** prev = &hc->clients;
while (*prev) {
if ((*prev)->closing) {
@@ -709,7 +850,8 @@ static void hc_cleanup(void* arg) {
u_free(dead); hc->client_count--;
} else prev = &(*prev)->next;
}
if (hc->ua) uasync_set_timeout(hc->ua, 5000, hc, hc_cleanup, "hc_cleanup");
hc->cleanup_timer = uasync_set_timeout(hc->ua, 5000, hc, hc_cleanup, "hc_cleanup");
if (!hc->cleanup_timer) DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: cleanup timer allocation failed");
}
/* ── Init / Destroy ── */
@@ -743,11 +885,15 @@ int chat_headless_control_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
}
if (listen(hc->listen_fd, 5) < 0) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; }
hc->listen_sock_id = uasync_add_socket_t(ua, hc->listen_fd, accept_callback, NULL, NULL, hc);
hc->listen_sock_id = uasync_add_socket_t(ua, hc->listen_fd, accept_callback, NULL, NULL, "chatctl_listen", hc);
if (!hc->listen_sock_id) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; }
chat_event_set_handler(inst, hc_on_chat_event);
uasync_set_timeout(ua, 5000, hc, hc_cleanup, "hc_cleanup");
hc->cleanup_timer = uasync_set_timeout(ua, 5000, hc, hc_cleanup, "hc_cleanup");
if (!hc->cleanup_timer) {
DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: cleanup timer allocation failed");
chat_headless_control_destroy(inst); return -1;
}
hc->running = 1;
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: listening on %s:%d", bind_ip, port);
@@ -756,13 +902,15 @@ int chat_headless_control_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
void chat_headless_control_destroy(struct UTUN_INSTANCE* inst) {
struct headless_control* hc = hc_of(inst);
if (!hc || !hc->running) return;
if (!hc) return;
hc->running = 0;
chat_event_set_handler(inst, NULL);
if (hc->cleanup_timer) uasync_cancel_timeout(hc->ua, hc->cleanup_timer);
if (inst->chat_event_handler == hc_on_chat_event) chat_event_set_handler(inst, NULL);
if (hc->listen_sock_id && hc->ua) uasync_remove_socket_t(hc->ua, hc->listen_fd);
if (hc->listen_fd != SOCKET_INVALID) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; }
struct headless_client* c = hc->clients;
while (c) { struct headless_client* n = c->next; if (c->fd != SOCKET_INVALID) socket_close_wrapper(c->fd); u_free(c); c = n; }
hc->clients = NULL; hc->client_count = 0;
while (c) { struct headless_client* n = c->next; hc_close_client(c); u_free(c); c = n; }
inst->headless = NULL;
u_free(hc);
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: destroyed");
}
+152 -37
View File
@@ -7,6 +7,7 @@
#include "chat_join.h"
#include "member_sync.h"
#include "chat_event.h"
#include "../utun_instance.h"
#include "../routing_layer/topo_node_sqlite.h"
@@ -57,10 +58,24 @@ static void _keys_purge_expired(struct UTUN_INSTANCE* inst) {
}
}
static void _keys_put(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key, uint64_t inviter_id) {
static int _keys_put(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key, uint64_t inviter_id) {
_keys_purge_expired(inst);
if (!channel_id || !join_key || !inviter_id) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid key registration", CJ_ID);
return -1;
}
for (struct join_key_entry* e = inst->join_keys; e; e = e->next) {
if (e->channel_id != channel_id || e->join_key != join_key) continue;
if (e->inviter_id != inviter_id) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration conflicts with existing inviter ch=%llu", CJ_ID,
(unsigned long long)channel_id);
return -1;
}
e->expiry_tb = _now_tb() + (uint64_t)JOIN_KEY_TTL_SECONDS * 10000ULL;
return 0;
}
struct join_key_entry* e = u_malloc(sizeof(*e));
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key alloc failed", CJ_ID); return; }
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key alloc failed", CJ_ID); return -1; }
e->join_key = join_key; e->channel_id = channel_id; e->inviter_id = inviter_id;
e->expiry_tb = _now_tb() + (uint64_t)JOIN_KEY_TTL_SECONDS * 10000ULL;
e->next = inst->join_keys;
@@ -68,6 +83,7 @@ static void _keys_put(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key stored key=%016llx ch=%llu inviter=%016llx ttl=%ds",
CJ_ID, (unsigned long long)join_key, (unsigned long long)channel_id,
(unsigned long long)inviter_id, JOIN_KEY_TTL_SECONDS);
return 0;
}
static struct join_key_entry* _keys_find(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) {
@@ -184,7 +200,6 @@ void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id,
return;
}
if (rc & MS_APPLY_CHANGED) {
member_sync_broadcast_one(inst, ch_id, m.node_id);
/* джойнер авторизован — добавляем в CHAT-группу сразу, чтобы BGP подключился */
uint64_t gid = strtoull(ch_id, NULL, 10);
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid);
@@ -196,12 +211,80 @@ void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id,
CJ_ID, (unsigned long long)m.node_id, ch_id);
}
}
/* уведомить GUI инвайтера об успешном присоединении джойнера */
uint8_t cevt[20]; memcpy(cevt, &m.node_id, 8);
int r = 0; memcpy(cevt + 8, &r, 4);
memcpy(cevt + 12, &group_id, 8);
chat_event_post(inst, CHAT_EVT_CONNECT_RESULT, cevt, 20);
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: process_request accepted joiner=0x%016llx ch=%s via=0x%016llx key=%016llx — signed by me",
CJ_ID, (unsigned long long)m.node_id, ch_id, (unsigned long long)from_node, (unsigned long long)join_key);
}
/* Ожидание прикладного ACK: надёжная доставка router сама повторяет потерянные пакеты. */
struct chat_join_registration {
struct chat_join_registration* next;
struct UTUN_INSTANCE* inst;
uint64_t channel_id, target_node_id, join_key;
void* timer;
chat_join_registered_fn callback;
void* arg;
};
static int send_key_control(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target,
uint64_t key, uint8_t subcmd, uint8_t status) {
struct ll_entry* entry = ll_alloc_lldgram(subcmd == JOIN_SUBCMD_KEY_REGISTER_ACK ? 11 : 10);
if (!entry) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: key control allocation failed", CJ_ID);
return -1;
}
entry->dgram[0] = ETCP_RT_ID_JOIN;
entry->len = entry->memlen;
entry->dgram[1] = subcmd;
memcpy(entry->dgram + 2, &key, 8);
if (subcmd == JOIN_SUBCMD_KEY_REGISTER_ACK) entry->dgram[10] = status;
int rc = etcp_route_send(inst, channel_id, target, entry, 0, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT);
if (rc != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: key control send failed cmd=%02x ch=%llu peer=%016llx rc=%d",
CJ_ID, subcmd, (unsigned long long)channel_id, (unsigned long long)target, rc);
}
return rc;
}
static void registration_finish(struct chat_join_registration* req, int result) {
struct chat_join_registration** p = &req->inst->join_registrations;
while (*p && *p != req) p = &(*p)->next;
if (*p) *p = req->next;
if (req->timer) uasync_cancel_timeout(req->inst->ua, req->timer);
if (result == CHAT_JOIN_OK) {
if (chat_join_store_local_key(req->inst, req->channel_id, req->join_key) != 0) result = CHAT_JOIN_ERROR;
}
if (result == CHAT_JOIN_OK || result == CHAT_JOIN_CANCELLED) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration %s ch=%llu peer=%016llx key=%016llx", CJ_ID,
result == CHAT_JOIN_OK ? "confirmed" : "cancelled", (unsigned long long)req->channel_id,
(unsigned long long)req->target_node_id, (unsigned long long)req->join_key);
} else {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration failed ch=%llu peer=%016llx key=%016llx result=%d", CJ_ID,
(unsigned long long)req->channel_id, (unsigned long long)req->target_node_id,
(unsigned long long)req->join_key, result);
}
chat_join_registered_fn callback = req->callback;
void* arg = req->arg;
u_free(req);
callback(arg, result);
}
static void registration_timer(void* arg) {
struct chat_join_registration* req = arg;
req->timer = NULL;
registration_finish(req, req->target_node_id == req->inst->node_id ? CHAT_JOIN_OK : CHAT_JOIN_TIMEOUT);
}
void chat_join_cancel_registration(struct chat_join_registration* req) {
if (req) registration_finish(req, CHAT_JOIN_CANCELLED);
}
/* ── etcp_router recv ── */
static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
@@ -215,6 +298,12 @@ static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
const uint8_t* d = entry->dgram;
uint64_t from_node; memcpy(&from_node, d + ROUTER_SVC_SRC_OFF, 8);
uint64_t group_id; memcpy(&group_id, d + ROUTER_SVC_GROUP_OFF, 8);
uint8_t required = ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED;
if (from_node != inst->node_id && (d[ROUTER_SVC_FLAGS_OFF] & required) != required) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: unprotected join packet rejected from=%016llx ch=%llu", CJ_ID,
(unsigned long long)from_node, (unsigned long long)group_id);
queue_dgram_free(entry); queue_entry_free(entry); return;
}
const uint8_t* payload = d + ROUTER_SVC_PAYLOAD_OFF;
size_t plen = entry->len - ROUTER_SVC_PAYLOAD_OFF;
uint8_t subcmd = payload[0];
@@ -224,9 +313,28 @@ static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (subcmd == JOIN_SUBCMD_KEY_REGISTER) {
/* payload: [subcmd:1][join_key:8] */
if (plen < 1 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; }
uint64_t join_key; memcpy(&join_key, payload + 1, 8);
_keys_put(inst, group_id, join_key, from_node);
if (plen != 9) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: malformed KEY_REGISTER len=%zu", CJ_ID, plen);
} else {
uint64_t key; memcpy(&key, payload + 1, 8);
int rc = _keys_put(inst, group_id, key, from_node);
send_key_control(inst, group_id, from_node, key, JOIN_SUBCMD_KEY_REGISTER_ACK, rc ? 1 : 0);
}
} else if (subcmd == JOIN_SUBCMD_KEY_REGISTER_ACK) {
if (plen != 10 || payload[9] > 1) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: malformed KEY_REGISTER_ACK len=%zu", CJ_ID, plen);
} else {
uint64_t key; memcpy(&key, payload + 1, 8);
struct chat_join_registration* req = inst->join_registrations;
while (req && (req->channel_id != group_id || req->target_node_id != from_node || req->join_key != key)) req = req->next;
/* Освобождаем пакет до callback: владелец может закрыть instance. */
int result = payload[9] ? CHAT_JOIN_ERROR : CHAT_JOIN_OK;
queue_dgram_free(entry); queue_entry_free(entry);
if (req) registration_finish(req, result);
else DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: late/unmatched registration ACK ch=%llu peer=%016llx", CJ_ID,
(unsigned long long)group_id, (unsigned long long)from_node);
return;
}
} else if (subcmd == JOIN_SUBCMD_REQUEST_FWD) {
/* payload: [subcmd:1][join_key:8][member...] */
if (plen < 1 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; }
@@ -243,6 +351,7 @@ static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
int chat_join_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
inst->join_stopping = 0;
if (etcp_router_bind(inst, ETCP_RT_ID_JOIN, join_recv_cb) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: etcp_router_bind failed", CJ_ID);
return -1;
@@ -253,45 +362,52 @@ int chat_join_init(struct UTUN_INSTANCE* inst) {
void chat_join_destroy(struct UTUN_INSTANCE* inst) {
if (!inst) return;
etcp_router_unbind(inst, ETCP_RT_ID_JOIN);
inst->join_stopping = 1;
if (inst->router_bindings.callbacks[ETCP_RT_ID_JOIN]) etcp_router_unbind(inst, ETCP_RT_ID_JOIN);
while (inst->join_registrations) chat_join_cancel_registration(inst->join_registrations);
_keys_free_all(inst);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CJ_ID);
}
/* ── Публичные операции ── */
void chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id,
uint64_t target_node_id, uint64_t join_key) {
if (!inst || channel_id == 0 || join_key == 0) return;
int chat_join_store_local_key(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) {
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: local registration without instance", CJ_ID); return -1; }
return _keys_put(inst, channel_id, join_key, inst->node_id);
}
/* инвайтер всегда хранит выданный ключ локально — для верификации REQUEST_FWD */
_keys_put(inst, channel_id, join_key, inst->node_id);
if (target_node_id == inst->node_id || target_node_id == 0) {
return;
struct chat_join_registration* chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id,
uint64_t target_node_id, uint64_t join_key, chat_join_registered_fn callback, void* arg) {
if (!inst || !inst->ua || inst->join_stopping || !channel_id || !join_key || !callback) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid registration arguments", CJ_ID);
return NULL;
}
uint8_t payload[1 + 8];
payload[0] = JOIN_SUBCMD_KEY_REGISTER;
memcpy(payload + 1, &join_key, 8);
struct ll_entry* entry = queue_entry_new(0);
if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: register_key queue_entry_new failed", CJ_ID); return; }
entry->dgram = u_malloc(1 + sizeof(payload));
if (!entry->dgram) { queue_entry_free(entry); return; }
entry->dgram[0] = ETCP_RT_ID_JOIN;
memcpy(entry->dgram + 1, payload, sizeof(payload));
entry->len = 1 + (uint16_t)sizeof(payload);
int rc = etcp_route_send(inst, channel_id, target_node_id, entry, 1, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT);
if (rc != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: register_key etcp_route_send FAILED rc=%d ch=%llu → 0x%016llx",
CJ_ID, rc, (unsigned long long)channel_id, (unsigned long long)target_node_id);
queue_dgram_free(entry); queue_entry_free(entry);
return;
for (struct chat_join_registration* r = inst->join_registrations; r; r = r->next) {
if (r->channel_id == channel_id && r->join_key == join_key) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration already pending ch=%llu", CJ_ID, (unsigned long long)channel_id);
return NULL;
}
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key registered ch=%llu → connection=0x%016llx key=%016llx",
CJ_ID, (unsigned long long)channel_id, (unsigned long long)target_node_id, (unsigned long long)join_key);
struct chat_join_registration* req = u_calloc(1, sizeof(*req));
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration allocation failed", CJ_ID); return NULL; }
req->inst = inst; req->channel_id = channel_id; req->join_key = join_key;
req->target_node_id = target_node_id ? target_node_id : inst->node_id;
req->callback = callback; req->arg = arg;
int local = req->target_node_id == inst->node_id;
req->timer = uasync_set_timeout(inst->ua, local ? 0 : JOIN_REGISTER_TIMEOUT_TB, req, registration_timer, "join_register");
if (!req->timer) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration timer allocation failed", CJ_ID);
u_free(req); return NULL;
}
if (!local && send_key_control(inst, channel_id, req->target_node_id, join_key, JOIN_SUBCMD_KEY_REGISTER, 0) != 0) {
uasync_cancel_timeout(inst->ua, req->timer);
u_free(req); return NULL;
}
req->next = inst->join_registrations;
inst->join_registrations = req;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration pending ch=%llu peer=%016llx key=%016llx", CJ_ID,
(unsigned long long)channel_id, (unsigned long long)req->target_node_id, (unsigned long long)join_key);
return req;
}
uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) {
@@ -330,7 +446,6 @@ int chat_join_forward_request(struct UTUN_INSTANCE* inst, uint64_t channel_id,
if (rc != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: forward etcp_route_send FAILED rc=%d ch=%llu → 0x%016llx",
CJ_ID, rc, (unsigned long long)channel_id, (unsigned long long)inviter_id);
queue_dgram_free(entry); queue_entry_free(entry);
return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: join request forwarded ch=%llu → inviter=0x%016llx joiner=0x%016llx",
+75 -14
View File
@@ -1,20 +1,71 @@
/*
* chat_join.h — авторизация входа в канал (join) через connection-узел и инвайтера
* chat_join — добавление участника канала через узел подключения C и инвайтера A.
* Модуль регистрирует invite-ключи, пересылает запрос к A и сохраняет подписанного
* участника. Прямой обмен с новым узлом J выполняет chat_sync; Merkle разносит запись.
*
* Flow (замена старого CHANNEL_INFO_REQ/CHANNEL_JOIN/WELCOME):
* Протокол (A и C могут быть одним узлом):
*
* инвайтер A connection-узел C джойнер J
* │ KEY_REGISTER(key,ch) │ │
* ├──────etcp_router─────────▶│ хранит {key→A, 10 мин} │
* │◀── KEY_REGISTER_ACK ──────┤ │
* │ теперь ссылка готова │ │
* │ │◀─── JOIN_INFO_REQ(key) ──────┤ (J подключился к C по ссылке)
* │ │── JOIN_INFO_RESP ───────────▶│ (ch_x25519, ch_ed25519, ch_sig, C-мембер)
* │ │◀─── JOIN_REQUEST ────────────┤ (key + полный набор мембера J)
* │ │ (валиден ключ → BGP new_conn, pending status=sent)
* │ │ валиден ключ → только pending, БЕЗ BGP new_conn
* │◀── REQUEST_FWD ───────────┤ │
* │ verif join_sig, node_id=derive(x25519) │
* │ member_sync_put(J, signed_by=A, signature=sign(A, J_x25519||"group_join"))
* │ ── merkle broadcast ──▶ (рекорд J дошёл по merkle) │
* │ │── JOIN_READY(ch) ────────────▶│ → topo_group_new_conn + ensure_channel_ready
* │ │── JOIN_READY(ch) ────────────▶│ добавление подтверждено у C
*
* КОНТРАКТ ЧЛЕНСТВА (обязателен для всех путей подключения):
* - A и C уже участвуют в канале; A может совпадать с C.
* - NCD даёт транспорт J–C для протокола добавления. Наличие транспорта,
* invite-ключа или локального TOPO_GROUP не даёт участия J в BGP-группе.
* Инициатор удерживает отдельный NCD handle до результата/таймаута добавления;
* закрывать его на транспортном UP, рассчитывая на владельца BGP, запрещено.
* - После проверки JOIN_REQUEST инвайтер сохраняет мембера через member_sync.
* Merkle распространяет запись между существующими участниками.
* - Каждый узел разрешает групповую сессию J после принятия и сохранения
* записи J в СВОЕЙ таблице участников. Подтверждение всех узлов не требуется.
* - Проверка invite-ключа разрешает только обработку запроса добавления.
* Временного членства и раннего BGP new_conn до сохранения мембера нет.
* - JOIN_READY означает наличие записи J у C. Это не подтверждение доставки
* всем узлам и не окончание обмена топологией. BGP JOIN_GROUP / TABLE_COMPLETE
* из topo_group.h относятся к отдельной групповой сессии.
*
* ПОРЯДОК И ПРОВЕРКИ:
* 1. A регистрирует (канал, join_key) -> A на C на JOIN_KEY_TTL_SECONDS.
* Ссылку можно публиковать только после успешного KEY_REGISTER_ACK.
* При A==C регистрация и обработка запроса локальные.
* 2. J открывает NCD к C; C проверяет ключ из JOIN_INFO_REQ и возвращает
* подписанное описание канала и свою запись участника. J проверяет ответ.
* Создание локальной инфраструктуры канала ещё не добавляет J на других узлах.
* 3. C проверяет JOIN_REQUEST: ключ и привязку идентичности J к транспорту.
* Запрос идёт к A по существующему маршруту канала; маршрут через J не нужен.
* 4. A проверяет запись, подписывает приглашение и сохраняет мембера.
* После локального commit или принятия записи по Merkle C отправляет JOIN_READY.
* 5. JOIN_GROUP, пришедший раньше записи мембера, не создаёт сессию у получателя.
* После commit member_sync повторно инициирует групповое согласование
* с этим пиром, если транспорт уже установлен. Новый ETCP-коннект не требуется.
*
* ОШИБКИ, ПОВТОРЫ И ОТМЕНА:
* - Неверный/истёкший ключ, несовпадение идентичности или неверная подпись
* не создают мембера. Ошибка/таймаут не дают временных прав.
* - Повторная запись проходит обычную проверку подписей и версий member_sync;
* повтор не создаёт второго мембера или второго владельца групповой сессии.
* - Ответ относится к ожидаемым каналу и пиру; ответ другой попытки не должен
* завершать текущее ожидание. UP другого пира не меняет адресата приглашения.
* - Отмена ожидания не откатывает уже сохранённую/реплицированную запись.
* Удаление локального канала отменяет его ожидания и подключения.
* - Recovery восстанавливает сессии существующих участников и не использует
* invite-ключ как замену локальной таблицы участников.
*
* J <-> C: ETCP_RT_ID_CHAT_SYNC, заголовок [service:1][group_id:8][type:1], payload
* описан в chat_sync.h. A <-> C: ETCP_RT_ID_JOIN через etcp_router с подписью
* и шифрованием; subcommands и payload описаны ниже.
*
* Модуль отвечает только за маршрутизируемую (etcp_router) часть:
* - регистрация ключа (инвайтер → connection),
@@ -35,24 +86,29 @@ struct UTUN_INSTANCE;
extern "C" {
#endif
/* etcp_router service id (connection ↔ инвайтер) */
#define ETCP_RT_ID_JOIN 0x33
/* etcp_router service id (connection ↔ инвайтер): ETCP_RT_ID_JOIN (см. etcp_api.h) */
/* subcommands (первый байт payload etcp_router) */
#define JOIN_SUBCMD_KEY_REGISTER 0x01 /* инвайтер → connection: {join_key:8} */
#define JOIN_SUBCMD_REQUEST_FWD 0x02 /* connection → инвайтер: {join_key:8, member...} */
#define JOIN_SUBCMD_KEY_REGISTER_ACK 0x03 /* connection → инвайтер: {join_key:8, status:1}; 0=stored, 1=rejected */
#define JOIN_KEY_TTL_SECONDS 600 /* 10 минут хранения ключа на connection-узле */
#define JOIN_REGISTER_TIMEOUT_TB 50000 /* 5 секунд на подтверждение */
enum chat_join_result { CHAT_JOIN_OK = 0, CHAT_JOIN_ERROR = -1, CHAT_JOIN_TIMEOUT = -2, CHAT_JOIN_CANCELLED = -3 };
struct chat_join_registration;
typedef void (*chat_join_registered_fn)(void* arg, int result);
/* Полный набор мембера джойнера (без подписи дерева — её ставит инвайтер). */
struct join_member_data {
uint64_t node_id;
uint64_t node_id; /* derive(x25519), идентичность нового участника */
const uint8_t* x25519; /* 32 */
const uint8_t* ed25519; /* 32 */
const uint8_t* join_sig; /* 64, может быть NULL */
uint64_t join_ts;
uint64_t join_ts; /* время подписи вступления, Unix seconds */
const uint8_t* update_sig; /* 64, может быть NULL */
uint64_t update_ts;
uint64_t update_ts; /* версия подписанного блока участника */
char userinfo[256]; /* JSON, null-terminated */
};
@@ -66,13 +122,18 @@ int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_
/* Инициализация: etcp_router_bind(ETCP_RT_ID_JOIN). */
int chat_join_init(struct UTUN_INSTANCE* inst);
/* Деинициализация. */
/* Снять обработчик, освободить ключи; ожидающие регистрации получают CANCELLED. */
void chat_join_destroy(struct UTUN_INSTANCE* inst);
/* Инвайтер: зарегистрировать join-ключ на connection-узле.
* target==self → храним локально (инвайтер и есть connection-узел). */
void chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id,
uint64_t target_node_id, uint64_t join_key);
/* Локальное приглашение (инвайтер == connection). 0=stored, -1=ошибка. */
int chat_join_store_local_key(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key);
/* Все вызовы в uasync-потоке. NULL=ошибка запуска, callback не вызывается.
* Иначе callback ровно один раз, после возврата register_key (также для self).
* Handle действителен до callback. cancel/destroy вызывают callback синхронно с CANCELLED. */
struct chat_join_registration* chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id,
uint64_t target_node_id, uint64_t join_key, chat_join_registered_fn callback, void* arg);
void chat_join_cancel_registration(struct chat_join_registration* registration);
/* Connection-узел: найти инвайтера по ключу. 0 = нет/истёк. */
uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key);
-4
View File
@@ -43,10 +43,6 @@ static uint8_t parse_adm_tags_flags(const char* adm_tags) {
if (json_flat_get(adm_tags, "supernode", buf, sizeof(buf)) == 0
&& strcmp(buf, "yes") == 0) flags |= CHAT_MEMBER_FLAG_SUPERNODE;
/* dm_storage: узел хранит offline-сообщения прямого чата (DM) */
if (json_flat_get(adm_tags, "dm_storage", buf, sizeof(buf)) == 0
&& strcmp(buf, "yes") == 0) flags |= CHAT_MEMBER_FLAG_DM_STORAGE;
/* admin: значение — накопительная строка ed...ed..., последний символ e/d */
if (json_flat_get(adm_tags, "admin", buf, sizeof(buf)) == 0) {
const char* last = buf;
-1
View File
@@ -30,7 +30,6 @@ extern "C" {
#define CHAT_MEMBER_FLAG_ADMIN 0x02
#define CHAT_MEMBER_FLAG_MODER 0x04
#define CHAT_MEMBER_FLAG_DELETED 0x08
#define CHAT_MEMBER_FLAG_DM_STORAGE 0x10
#define CHAT_MEMBER_DISPLAY_SIZE 79
+110 -18
View File
@@ -216,16 +216,19 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
u_free(sig_msg); u_free(json); u_free(full_data); u_free(mctx); return;
}
int ret = db_sync_insert_signed(si, json, strlen(json), json_sig, 64, ts, NULL);
/* local_attrs готовим заранее и пишем атомарно при вставке — иначе GUI по
MSG_RECEIVED может прочитать сообщение раньше отдельного UPDATE (гонка),
и своё голосовое покажется «без медиа» (серый media bar + попытка скачать) */
const char* base = strrchr(mctx->media_dest, '/');
const char* filename = base ? base + 1 : mctx->media_dest;
char attrs[1100]; snprintf(attrs, sizeof(attrs), "{\"st\":\"fl\",\"fp\":\"%s\"}", filename);
int ret = db_sync_insert_signed(si, json, strlen(json), json_sig, 64, ts, attrs);
if (ret != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media insert failed ret=%d", CC_ID, ret);
} else {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: media message inserted ch=%s ts=%llu",
CC_ID, mctx->channel_id, (unsigned long long)ts);
const char* base = strrchr(mctx->media_dest, '/');
const char* filename = base ? base + 1 : mctx->media_dest;
char attrs[1100]; snprintf(attrs, sizeof(attrs), "{\"st\":\"fl\",\"fp\":\"%s\"}", filename);
chat_core_update_local_attrs(mctx->inst, mctx->channel_id, ts, json_sig, attrs);
/* анонсируем блоки суперузлам — иначе первый скачивающий не найдёт держателя */
media_delivery_announce_media(mctx->inst, strtoull(mctx->channel_id, NULL, 10),
@@ -339,8 +342,8 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
}
if (req->media_video) {
video_transcode_start(inst->ua, req->media_src, req->media_dest,
on_video_prepared, mctx);
if (video_transcode_start(inst->media_async, inst->ua, req->media_src, req->media_dest, on_video_prepared, mctx) < 0)
on_video_prepared(mctx, -1);
return;
}
@@ -838,19 +841,23 @@ static int md_auto_download(struct UTUN_INSTANCE* inst,
/* ─── db_sync callback ─── */
void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, void* arg) {
(void)si;
struct msg_insert_arg* ia = (struct msg_insert_arg*)arg;
struct UTUN_INSTANCE* inst = ia ? ia->inst : NULL;
void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, int initial_sync, void* arg) {
struct UTUN_INSTANCE* inst = arg;
struct chat_core_ctx* cc = CC(inst);
if (!ia || !cc) return;
const char* ch_id = ia->ch_id;
if (!cc) return;
char ch_id[32]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)db_sync_instance_group_id(si));
uint8_t evt[73]; uint8_t cl = (uint8_t)strlen(ch_id); evt[0] = cl; memcpy(evt + 1, ch_id, cl);
memcpy(evt + 1 + cl, &author, 8);
chat_event_post(inst, CHAT_EVT_MSG_RECEIVED, evt, 1 + cl + 8);
/* auto-download media if message contains media metadata */
if (data && len > 0 && author != cc->my_node_id) {
/* auto-download media if message contains media metadata (live PUSH/local only).
initial-sync (SEND_DATA) media докачивается после sync через per-channel backfill. */
if (initial_sync && data && len > 0 && author != cc->my_node_id) {
char dbg[4096]; size_t dbgl = len < sizeof(dbg) - 1 ? len : sizeof(dbg) - 1;
memcpy(dbg, data, dbgl); dbg[dbgl] = '\0';
if (strstr(dbg, "\"ct\":\"") && strstr(dbg, "\"d\":\""))
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "%s: media download deferred (initial sync) ch=%s", CC_ID, ch_id);
} else if (data && len > 0 && author != cc->my_node_id) {
/* data from db_sync may not be null-terminated; copy to local buffer */
char buf[4096]; size_t blen = len < sizeof(buf) - 1 ? len : sizeof(buf) - 1;
memcpy(buf, data, blen); buf[blen] = '\0';
@@ -961,6 +968,10 @@ void chat_core_attachment_download(struct UTUN_INSTANCE* inst, const char* chann
uint8_t author_sig[64]; memcpy(author_sig, sig_blob, 64);
sqlite3_finalize(st);
/* сбрасываем пометку «файл есть»: файл мог быть вычищен кешем, а local_attrs ещё "fl".
по завершении md_download_done_cb снова запишет st=fl (успех) или st=er (ошибка). */
chat_core_update_local_attrs(inst, channel_id, (uint64_t)db_ts, author_sig, "{}");
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: manual download ch=%s id=%lld ct=%s file=%s",
CC_ID, channel_id, (long long)msg_id, content_type, base_filename);
md_start_download(inst, body, bi, channel_id, base_filename, (uint64_t)db_ts, author_sig, (uint64_t)author_node_id, msg_id, content_type);
@@ -1114,14 +1125,15 @@ static int bf_cmp_ts_desc(const void* a, const void* b) {
return 0;
}
/* Собирает все медиасообщения всех каналов (presence + размеры + контекст докачки). */
static void bf_collect(struct chat_core_ctx* cc, const char* media_base, struct bf_list* l) {
/* Собирает медиасообщения (presence + размеры + контекст докачки). only_ch_id != NULL — только этот канал. */
static void bf_collect(struct chat_core_ctx* cc, const char* media_base, const char* only_ch_id, struct bf_list* l) {
sqlite3_stmt* cs = NULL;
if (sqlite3_prepare_v2(cc->db, "SELECT channel_id FROM channels", -1, &cs, NULL) != SQLITE_OK) return;
while (sqlite3_step(cs) == SQLITE_ROW) {
const char* ch_id = (const char*)sqlite3_column_text(cs, 0);
if (!ch_id || !ch_id[0]) continue;
if (only_ch_id && strcmp(only_ch_id, ch_id) != 0) continue;
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char sql[256];
@@ -1221,7 +1233,7 @@ void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst) {
else snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
struct bf_list list = {0};
bf_collect(cc, media_base, &list);
bf_collect(cc, media_base, NULL, &list);
if (list.count > 1) qsort(list.items, (size_t)list.count, sizeof(struct bf_entry), bf_cmp_ts_desc);
/* инициализация счётчика: сумма всех present-файлов */
@@ -1273,6 +1285,51 @@ void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst) {
chat_media_cache_reconcile(inst);
}
/* Докачка медиа одного канала после завершения его chat_sync.
Порядок — от последних сообщений назад (список сортируется desc по ts). */
void chat_media_autodownload_backfill_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db || !inst || !ch_id || !ch_id[0]) return;
uint64_t limit = inst->config->global.chatserver_storage_total_size;
int autoload = chat_setting_get_int(inst, "storage_autoload", 1);
int days = chat_setting_get_int(inst, "storage_backfill_days", 7);
uint64_t now_ms = (uint64_t)ntp_time_get_seconds(inst) * 1000ULL;
uint64_t cutoff_ms = now_ms - (uint64_t)days * 86400ULL * 1000ULL;
char media_base[512];
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
else snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
struct bf_list list = {0};
bf_collect(cc, media_base, ch_id, &list);
if (list.count > 1) qsort(list.items, (size_t)list.count, sizeof(struct bf_entry), bf_cmp_ts_desc);
uint64_t used = cc->media_cache_bytes;
int downloaded = 0, skipped = 0;
for (int i = 0; i < list.count; i++) {
struct bf_entry* e = &list.items[i];
if (e->actual_size > 0) continue;
if (!autoload || e->author == cc->my_node_id) { skipped++; continue; }
if (e->ts_ms < cutoff_ms) { skipped++; continue; }
if (limit > 0 && used + (uint64_t)e->claimed_size > limit) { skipped++; continue; }
if (md_auto_download(inst, e->body, e->body_len, e->ch_id, e->base_filename,
e->ts_ms, e->author_sig, e->author, e->msg_id, e->content_type)) {
downloaded++;
used += (uint64_t)e->claimed_size;
} else {
skipped++;
}
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: channel backfill done ch=%s downloaded=%d skipped=%d",
CC_ID, ch_id, downloaded, skipped);
for (int i = 0; i < list.count; i++) if (list.items[i].body) u_free(list.items[i].body);
u_free(list.items);
}
/* ─── Кеш медиафайлов: очистка сирот + лимит storage_total_size ─── */
struct media_msg_file {
@@ -1559,22 +1616,57 @@ static void media_startup_backfill_done_cb(struct DB_SYNC_INSTANCE* si, uint64_t
media_startup_backfill_timer_cb, "chat_media_bl");
}
/* per-channel: после SYNC_DONE канала — докачка его медиа (дебаунс 2s, от новых к старым) */
static void media_channel_backfill_timer_cb(void* arg) {
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
struct chat_core_ctx* cc = CC(inst);
cc->media_channel_backfill_timer = NULL;
for (int i = 0; i < cc->media_channel_backfill_pending; i++)
chat_media_autodownload_backfill_channel(inst, cc->media_channel_backfill[i]);
cc->media_channel_backfill_pending = 0;
}
static void media_channel_backfill_done_cb(struct DB_SYNC_INSTANCE* si, uint64_t peer_node_id, void* arg) {
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
struct chat_core_ctx* cc = CC(inst);
(void)peer_node_id;
if (!cc || !si) return;
char ch_id[32]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)db_sync_instance_group_id(si));
for (int i = 0; i < cc->media_channel_backfill_pending; i++)
if (strcmp(cc->media_channel_backfill[i], ch_id) == 0) return;
if (cc->media_channel_backfill_pending < (int)(sizeof(cc->media_channel_backfill) / sizeof(cc->media_channel_backfill[0])))
snprintf(cc->media_channel_backfill[cc->media_channel_backfill_pending++],
sizeof(cc->media_channel_backfill[0]), "%s", ch_id);
if (cc->media_channel_backfill_timer) uasync_cancel_timeout(inst->ua, cc->media_channel_backfill_timer);
cc->media_channel_backfill_timer = uasync_set_timeout(inst->ua, 20000, inst,
media_channel_backfill_timer_cb, "chat_md_chbl");
}
void chat_media_startup_backfill_init(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!inst || !cc) return;
cc->media_backfill_inst = inst;
cc->media_backfill_ran = 0;
cc->media_channel_backfill_pending = 0;
cc->media_channel_backfill_timer = NULL;
db_sync_add_done_cbk(inst, media_startup_backfill_done_cb, inst);
db_sync_add_done_cbk(inst, media_channel_backfill_done_cb, inst);
}
void chat_media_startup_backfill_destroy(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!inst || !cc) return;
db_sync_remove_done_cbk(inst, media_startup_backfill_done_cb, inst);
db_sync_remove_done_cbk(inst, media_channel_backfill_done_cb, inst);
if (cc->media_backfill_timer) {
uasync_cancel_timeout(inst->ua, cc->media_backfill_timer);
cc->media_backfill_timer = NULL;
}
if (cc->media_channel_backfill_timer) {
uasync_cancel_timeout(inst->ua, cc->media_channel_backfill_timer);
cc->media_channel_backfill_timer = NULL;
}
cc->media_channel_backfill_pending = 0;
cc->media_backfill_inst = NULL;
cc->media_backfill_ran = 0;
}
-1
View File
@@ -99,7 +99,6 @@ void chat_core_update_my_member(struct UTUN_INSTANCE* inst) {
inst->my_ed25519_pubkey, join_sig, join_ts,
update_sig, update_ts, juser, NULL, NULL, 0, 0, NULL);
if (r2 < 0) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member_sync_put(self) FAILED ch=%s rc=%d", CC_ID, ch, r2);
else if (r2 & MS_APPLY_CHANGED) member_sync_broadcast_one(inst, ch, myid);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: update my member ch=%s ts=%llu", CC_ID, ch,
(unsigned long long)update_ts);
+9 -3
View File
@@ -34,14 +34,20 @@ struct chat_setting_def {
static const struct chat_setting_def g_setting_defs[] = {
{"storage_autoload", CHAT_SETTING_BOOL, 1, 0, 1},
{"opus_codec_preset", CHAT_SETTING_INT, 1, 0, 4},
{"compressor_enabled", CHAT_SETTING_BOOL, 0, 0, 1},
{"compressor_max_gain_db", CHAT_SETTING_INT, 25, 1, 100},
{"compressor_rise_rate", CHAT_SETTING_INT, 10, 1, 100},
{"radio_opus_mode", CHAT_SETTING_INT, 0, 0, 1},
{"radio_opus_bitrate", CHAT_SETTING_INT, 32, 16, 128},
{"radio_capture_stereo", CHAT_SETTING_INT, 0, 0, 1},
{"compressor_enabled", CHAT_SETTING_BOOL, 1, 0, 1},
{"compressor_max_gain_db", CHAT_SETTING_INT, 15, 1, 100},
{"compressor_rise_rate", CHAT_SETTING_INT, 2, 1, 100},
{"radio_compressor_enabled", CHAT_SETTING_BOOL, 1, 0, 1},
{"radio_compressor_max_gain_db",CHAT_SETTING_INT, 15, 1, 100},
{"media_download_max_peers", CHAT_SETTING_INT, 3, 1, 16},
{"storage_backfill_days", CHAT_SETTING_INT, 7, 1, 365},
{"standby_active_sec", CHAT_SETTING_INT, 2, 1, 3600},
{"standby_sleep_sec", CHAT_SETTING_INT, 60, 1, 86400},
{"standby_min_sleep_sec", CHAT_SETTING_INT, 15, 1, 86400},
{"standby_keepalive_ms", CHAT_SETTING_INT, 30000, 1000, 600000},
{"join_policy", CHAT_SETTING_INT, 1, 0, 2}, /* 0=autojoin, 1=ask, 2=deny */
{"whisper_enabled", CHAT_SETTING_BOOL, 0, 0, 1},
{"whisper_model_path", CHAT_SETTING_STRING, 0, 0, 0},
+2 -2
View File
@@ -218,7 +218,7 @@ static void collect_conn_metrics(struct UTUN_INSTANCE* inst, uint64_t peer_node_
"=== PEER: %04llX\u2192%04llX [%s] ===\n"
"Role: %s\n"
"Status: %s Links: %d/%d Initialized: %s MTU: %d\n"
"Reinit: %u Reset: %u Tx_state: %d Routing_exchange: %d\n"
"Reinit: %u Reset: %u Tx_state: %d\n"
"\n--- ETCP ---\n"
"RTT last/avg10: %.1f/%.1f ms Jitter: %.1f ms\n"
"Bytes sent: %u Retrans: %u ACKs: %u\n"
@@ -230,7 +230,7 @@ static void collect_conn_metrics(struct UTUN_INSTANCE* inst, uint64_t peer_node_
role_str,
conn->links_up ? "UP" : "DOWN", links_up_count, link_count,
conn->initialized ? "yes" : "no", conn->mtu,
conn->reinit_count, conn->reset_count, conn->tx_state, conn->routing_exchange_active,
conn->reinit_count, conn->reset_count, conn->tx_state,
conn->rtt_last / 10.0f, conn->rtt_avg_10 / 10.0f, conn->jitter / 10.0f,
conn->bytes_sent_total, conn->retransmissions_count, conn->ack_packets_count,
conn->unacked_bytes, conn->max_inflight,
+221 -56
View File
@@ -17,6 +17,7 @@
#include "../utun_instance.h"
#include "../transport_layer/etcp_api.h"
#include "../transport_layer/etcp.h"
#include "../transport_layer/node_conn_direct.h"
#include "../transport_layer/secure_channel.h"
#include "../ntp_time.h"
#include "../../lib/u_async.h"
@@ -31,6 +32,7 @@
#include <string.h>
#include <sqlite3.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#ifdef __ANDROID__
#include <android/log.h>
#endif
@@ -72,10 +74,13 @@ struct chat_sync {
uint8_t pending_invite_is_inviter; /* 1 = inviter sends CHANNEL_INVITE on conn_up, 0 = joiner sends JOIN_INFO_REQ */
char pending_password[128]; /* пароль для JOIN_INFO_REQ */
uint64_t pending_join_key; /* join-ключ из invite-ссылки (джойнер) */
struct NODE_CONN_DIRECT* join_handle; /* транспорт протокола добавления, не членство */
void* join_transport_timer;
struct join_pending* join_pending; /* connection-узел: ожидающие ready джойнеры */
/* Pending incoming invites (для ask/deny) */
uint64_t pending_inv_in_ch_id[4];
uint64_t pending_inv_in_node_id[4];
uint64_t pending_inv_in_key[4];
int pending_inv_in_count;
};
@@ -85,6 +90,47 @@ struct chat_sync {
#define CS_INVITE_SEND_TIMEOUT_MS 3000 /* inviter waits for joiner to respond */
#define CS_SYNC_INTERVAL_MS 1000
static void cs_info_req_timeout_cb(void* arg);
static void cs_join_connect_timeout(void* arg) {
struct chat_sync* cs = arg;
cs->join_transport_timer = NULL;
cs_info_req_timeout_cb(cs);
}
static void cs_join_release_transport(struct chat_sync* cs) {
if (cs->join_transport_timer) {
uasync_cancel_timeout(cs->inst->ua, cs->join_transport_timer); cs->join_transport_timer = NULL;
}
struct NODE_CONN_DIRECT* handle = cs->join_handle;
cs->join_handle = NULL;
if (handle) {
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "join transport release peer=%016llx",
(unsigned long long)node_conn_direct_node_id(handle));
node_conn_direct_close(handle);
}
}
static int cs_join_hold_transport(struct chat_sync* cs, uint64_t node_id, struct TOPO_NODE* ni) {
struct NODE_CONN_DIRECT* handle = NULL;
int rc = ni ? node_conn_direct_open_node(cs->inst, node_id, NULL, NULL, &handle, ni, NULL)
: node_conn_direct_open(cs->inst, node_id, NULL, NULL, &handle, NULL);
if (rc == NCD_ERR) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "cannot acquire join transport peer=%016llx", (unsigned long long)node_id);
return -1;
}
cs_join_release_transport(cs);
cs->join_handle = handle;
cs->join_transport_timer = uasync_set_timeout(cs->inst->ua, CS_INFO_REQ_TIMEOUT_MS * 10,
cs, cs_join_connect_timeout, "join_transport_timeout");
if (!cs->join_transport_timer) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "cannot arm join transport timeout");
cs_join_release_transport(cs); return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "join owns transport peer=%016llx", (unsigned long long)node_id);
return 0;
}
static const char* cs_msg_name(uint8_t type) {
switch (type) {
case CS_MSG_INIT_SYNC: return "INIT_SYNC";
@@ -99,6 +145,7 @@ static const char* cs_msg_name(uint8_t type) {
case CS_MSG_JOIN_INFO_RESP: return "JOIN_INFO_RESP";
case CS_MSG_JOIN_REQUEST: return "JOIN_REQUEST";
case CS_MSG_JOIN_READY: return "JOIN_READY";
case CS_MSG_SOUND_SETTING: return "SOUND_SETTING";
default: return "???";
}
}
@@ -150,6 +197,7 @@ static int cs_send(struct chat_sync* cs, const char* ch_id, uint64_t dst,
struct ETCP_CONN* conn = cs_find_conn_for_node(cs->inst, dst);
if (!conn) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: no conn for node %016llx", CS_ID, (unsigned long long)dst); u_free(buf); queue_entry_free(entry); return -1; }
int rc = etcp_send(conn, entry);
if (rc != 0) { queue_dgram_free(entry); queue_entry_free(entry); }
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: etcp_send rc=%d id=0x%02x to=%016llx conn=%s", CS_ID, rc, payload[0], (unsigned long long)dst, conn->log_name);
return rc;
}
@@ -203,13 +251,15 @@ static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
const uint8_t* d = entry->dgram;
size_t dlen = entry->len;
uint64_t group_id = be64toh(*(const uint64_t*)(d + 1));
uint64_t group_id; memcpy(&group_id, d + 1, sizeof(group_id));
group_id = be64toh(group_id);
uint8_t type = d[9];
char ch_id[64];
if (cs_group_id_to_ch_id(cs->inst, group_id, ch_id, sizeof(ch_id)) != 0) {
/* CHANNEL_INVITE/JOIN_INFO_REQ приходят для ещё не созданного у нас канала:
group_id == channel_id (numeric), поэтому выводим ch_id напрямую. */
if (type == CS_MSG_CHANNEL_INVITE || type == CS_MSG_JOIN_INFO_REQ || type == CS_MSG_JOIN_REQUEST) {
if (type == CS_MSG_CHANNEL_INVITE || type == CS_MSG_JOIN_INFO_REQ || type == CS_MSG_JOIN_INFO_RESP ||
type == CS_MSG_JOIN_REQUEST || type == CS_MSG_JOIN_READY || type == CS_MSG_ERROR) {
snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)group_id);
} else {
u_free(entry->dgram); queue_entry_free(entry); return;
@@ -223,6 +273,13 @@ static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: RECV %s from=%016llx ch=%s len=%zu",
CS_ID, cs_msg_name(type), (unsigned long long)peer, ch_id, plen);
if ((type == CS_MSG_JOIN_INFO_RESP || type == CS_MSG_JOIN_READY || type == CS_MSG_ERROR) &&
(cs->pending_invite_node_id != peer || cs->pending_invite_ch_id != group_id)) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "ignore stale join response peer=%016llx group=%016llx type=%u",
(unsigned long long)peer, (unsigned long long)group_id, type);
u_free(entry->dgram); queue_entry_free(entry); return;
}
switch (type) {
case CS_MSG_JOIN_INFO_REQ: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_INFO_REQ", CS_ID); cs_handle_join_info_req(cs, peer, ch_id, pl, plen); break;
case CS_MSG_JOIN_INFO_RESP: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_INFO_RESP", CS_ID); cs_handle_join_info_resp(cs, peer, ch_id, pl, plen); break;
@@ -230,13 +287,18 @@ static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
case CS_MSG_JOIN_READY: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_READY", CS_ID); cs_handle_join_ready(cs, peer, ch_id, pl, plen); break;
case CS_MSG_ERROR: {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: RECV ERROR from=%016llx ch=%s", CS_ID, (unsigned long long)peer, ch_id);
if (cs->info_req_timer) { uasync_cancel_timeout(cs->inst->ua, cs->info_req_timer); cs->info_req_timer = NULL; }
cs_cancel_proto_timers(cs);
uint8_t err[20]; memcpy(err, &peer, 8); int r = -1; memcpy(err + 8, &r, 4);
memcpy(err + 12, &cs->pending_invite_ch_id, 8); chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, err, 20);
cs->pending_invite_node_id = 0; cs->pending_invite_ch_id = 0;
cs_join_release_transport(cs);
break;
}
case CS_MSG_CHANNEL_INVITE: cs_handle_channel_invite(cs, peer, ch_id, pl, plen); break;
case CS_MSG_SOUND_SETTING:
if (plen >= 1)
db_sync_set_peer_sound(cs->inst, group_id, peer, pl[0] ? 1 : 0);
break;
default: DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: UNKNOWN msg type=%02x from=%016llx", CS_ID, type, (unsigned long long)peer); break;
}
u_free(entry->dgram); queue_entry_free(entry);
@@ -257,6 +319,7 @@ static void cs_info_req_timeout_cb(void* arg) {
chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, err, 20);
cs->pending_invite_node_id = 0;
cs->pending_invite_ch_id = 0;
cs_join_release_transport(cs);
}
static void cs_join_timeout_cb(void* arg) {
@@ -273,6 +336,7 @@ static void cs_join_timeout_cb(void* arg) {
cs->pending_invite_node_id = 0;
cs->pending_invite_ch_id = 0;
cs->pending_invite_is_inviter = 0;
cs_join_release_transport(cs);
}
static void cs_invite_send_timeout_cb(void* arg) {
@@ -289,6 +353,7 @@ static void cs_invite_send_timeout_cb(void* arg) {
cs->pending_invite_node_id = 0;
cs->pending_invite_ch_id = 0;
cs->pending_invite_is_inviter = 0;
cs_join_release_transport(cs);
}
/* ── helper: find active ETCP_CONN for node ── */
@@ -392,6 +457,49 @@ static void cs_schedule_sync(struct chat_sync* cs) {
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: sync scheduled in %ums", CS_ID, CS_SYNC_INTERVAL_MS);
}
/* ── Peer sleep (keepalive-протокол): спячка → отменить сессии; пробуждение → пересинхронизация ── */
static void cs_on_peer_sleep(struct UTUN_INSTANCE* inst, uint64_t peer_node_id, int sleeping, void* arg) {
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
if (sleeping) {
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: peer sleep peer=0x%016llx — cancel member_sync sessions", CS_ID, (unsigned long long)peer_node_id);
merkle_sync_cancel_peer(inst, peer_node_id);
} else {
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: peer awake peer=0x%016llx — schedule sync", CS_ID, (unsigned long long)peer_node_id);
cs_schedule_sync(cs);
}
}
/* ── Per-chat «play sound on message» (троттлинг db_sync на сервере) ── */
static void cs_send_sound_setting(struct chat_sync* cs, const char* ch_id, uint64_t dst, int on) {
uint8_t p[2] = { CS_MSG_SOUND_SETTING, (uint8_t)(on ? 1 : 0) };
cs_send(cs, ch_id, dst, p, sizeof(p));
}
void chat_sync_broadcast_sound_setting(struct UTUN_INSTANCE* inst, const char* ch_id, int on) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !cs->initialized || !ch_id || !ch_id[0]) return;
for (int i = 0; i < cs->channel_count; i++) {
if (strcmp(cs->channels[i].channel_id, ch_id) != 0) continue;
for (int j = 0; j < cs->channels[i].peer_count; j++) {
uint64_t pid = cs->channels[i].peer_ids[j];
if (pid == inst->node_id || !cs_is_peer_online(inst, pid)) continue;
cs_send_sound_setting(cs, ch_id, pid, on);
}
break;
}
}
/* На поднятии соединения: отправить пиру свои per-chat настройки звука (начальная синхронизация). */
static void cs_send_all_sound_settings(struct chat_sync* cs, uint64_t peer) {
for (int i = 0; i < cs->channel_count; i++) {
int on = chat_core_get_sound_on_message(cs->inst, cs->channels[i].channel_id);
cs_send_sound_setting(cs, cs->channels[i].channel_id, peer, on);
}
}
/* ── Peer status change (GUI + online-бар + schedule_sync; онлайн в БД не хранится) ── */
static void cs_on_peer_status_changed(struct chat_sync* cs, uint64_t peer, int online) {
@@ -465,7 +573,10 @@ static void _on_invite_sync_done(uint64_t peer, const char* ns, int result, void
/* ── send JOIN_INFO_REQ to start join handshake ── */
static void cs_start_channel_join(struct chat_sync* cs, uint64_t peer) {
if (!cs || cs->info_req_timer) return;
if (!cs || cs->info_req_timer || cs->join_timer) return;
if (cs->join_transport_timer) {
uasync_cancel_timeout(cs->inst->ua, cs->join_transport_timer); cs->join_transport_timer = NULL;
}
char ch_id_str[64];
snprintf(ch_id_str, sizeof(ch_id_str), "%llu", (unsigned long long)cs->pending_invite_ch_id);
/* payload: [join_key:8][pass_len:1][password] */
@@ -495,11 +606,10 @@ static void cs_on_conn_up(struct ETCP_CONN* conn, int event, void* arg) { (void)
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn_up peer=%016llx pending_invite=%llu links_up=%d", CS_ID, (unsigned long long)peer, (unsigned long long)cs->pending_invite_ch_id, conn->links_up);
if (cs->pending_invite_ch_id != 0) {
if (cs->pending_invite_node_id != 0 && cs->pending_invite_node_id != peer)
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite node_id MISMATCH: invite=0x%016llx ETCP_peer=0x%016llx — invite is STALE!",
CS_ID, (unsigned long long)cs->pending_invite_node_id, (unsigned long long)peer);
cs->pending_invite_node_id = peer;
if (cs->pending_invite_ch_id != 0 && cs->pending_invite_node_id == peer) {
if (cs->join_transport_timer) {
uasync_cancel_timeout(cs->inst->ua, cs->join_transport_timer); cs->join_transport_timer = NULL;
}
if (cs->pending_invite_is_inviter) {
/* we are the inviter — send CHANNEL_INVITE to the joiner */
char ch_id[64]; snprintf(ch_id, sizeof(ch_id), "%llu",
@@ -520,6 +630,7 @@ static void cs_on_conn_up(struct ETCP_CONN* conn, int event, void* arg) { (void)
}
cs_on_peer_status_changed(cs, peer, 1);
cs_send_all_sound_settings(cs, peer);
}
static void cs_on_conn_down(struct ETCP_CONN* conn, int event, void* arg) { (void)event;
@@ -530,8 +641,11 @@ static void cs_on_conn_down(struct ETCP_CONN* conn, int event, void* arg) { (voi
cs_on_peer_status_changed(cs, peer, 0);
cs_cancel_proto_timers(cs);
if (cs->pending_invite_node_id == peer) {
cs_cancel_proto_timers(cs);
if (cs->join_transport_timer) uasync_cancel_timeout(cs->inst->ua, cs->join_transport_timer);
cs->join_transport_timer = uasync_set_timeout(cs->inst->ua, CS_INFO_REQ_TIMEOUT_MS * 10,
cs, cs_join_connect_timeout, "join_reconnect_timeout");
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn down while waiting invite resp peer=%016llx, timers cancelled, state kept for retry on reconnect",
CS_ID, (unsigned long long)peer);
/* invite state survives connection flaps: only timeout or explicit protocol end clears it */
@@ -548,6 +662,10 @@ static void cs_on_conn_down(struct ETCP_CONN* conn, int event, void* arg) { (voi
static void cs_on_conn_status(struct ETCP_CONN* conn, int status, void* arg) {
switch (status) {
case ETCP_CONN_STATUS_UP: cs_on_conn_up(conn, ETCP_CBK_EVENT_UP, arg); break;
case ETCP_CONN_STATUS_REINIT:
cs_on_conn_down(conn, ETCP_CBK_EVENT_DOWN, arg);
cs_on_conn_up(conn, ETCP_CBK_EVENT_UP, arg);
break;
case ETCP_CONN_STATUS_DOWN:
case ETCP_CONN_STATUS_DELETE: cs_on_conn_down(conn, ETCP_CBK_EVENT_DOWN, arg); break;
default: break;
@@ -622,6 +740,7 @@ static void refresh_timer_cb(void* arg) {
int chat_sync_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
if (inst->chat_sync) return 0;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: init", CS_ID);
struct chat_sync* cs = u_calloc(1, sizeof(*cs));
if (!cs) return -1;
@@ -629,6 +748,11 @@ int chat_sync_init(struct UTUN_INSTANCE* inst) {
cs->initialized = 1;
inst->chat_sync = cs;
if (member_sync_init(inst) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member synchronization initialization failed", CS_ID);
inst->chat_sync = NULL; u_free(cs); return -1;
}
etcp_bind(inst, ETCP_RT_ID_CHAT_SYNC, chat_sync_recv_cb);
etcp_add_conn_status_cbk(inst, cs_on_conn_status, cs);
@@ -639,8 +763,8 @@ int chat_sync_init(struct UTUN_INSTANCE* inst) {
cs->info_req_timer = NULL;
cs->join_timer = NULL;
member_sync_init(inst);
member_sync_add_apply_cbk(inst, cs_on_member_applied, cs);
utun_add_peer_sleep_cbk(inst, cs_on_peer_sleep, cs);
chat_admin_init(inst);
chat_join_init(inst);
@@ -656,6 +780,7 @@ void chat_sync_destroy(struct UTUN_INSTANCE* inst) {
if (cs->invite_send_timer) { uasync_cancel_timeout(inst->ua, cs->invite_send_timer); cs->invite_send_timer = NULL; }
if (cs->sync_scheduled) { cs->sync_scheduled = 0; cs_flush_sync(cs); }
member_sync_remove_apply_cbk(inst, cs_on_member_applied, cs);
utun_remove_peer_sleep_cbk(inst, cs_on_peer_sleep, cs);
cs_pending_free_all(cs);
member_sync_destroy(inst);
chat_admin_destroy(inst);
@@ -669,6 +794,7 @@ void chat_sync_destroy(struct UTUN_INSTANCE* inst) {
if (cs->refresh_wait) { standby_wait_cancel(cs->refresh_wait); cs->refresh_wait = NULL; }
#endif
cs_cancel_proto_timers(cs);
cs_join_release_transport(cs);
for (int i = 0; i < cs->channel_count; i++)
if (cs->channels[i].peer_ids) u_free(cs->channels[i].peer_ids);
@@ -698,6 +824,11 @@ void chat_sync_connect_node(struct UTUN_INSTANCE* inst, uint64_t node_id) {
void chat_sync_on_channel_deleted(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !ch_id || !ch_id[0]) return;
if (cs->pending_invite_ch_id == strtoull(ch_id, NULL, 10)) {
cs_cancel_proto_timers(cs);
cs->pending_invite_ch_id = 0; cs->pending_invite_node_id = 0;
cs_join_release_transport(cs);
}
for (int i = 0; i < cs->channel_count; i++) {
if (strcmp(cs->channels[i].channel_id, ch_id) != 0) continue;
if (cs->channels[i].peer_ids) u_free(cs->channels[i].peer_ids);
@@ -832,13 +963,13 @@ static void cm_invite_trampoline(void* arg) {
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
"channel invite: link to 0x%016llx already up — skipping connect, join handshake directly",
(unsigned long long)node_id);
cs_start_channel_join(cs, node_id);
if (cs_join_hold_transport(cs, node_id, NULL) == 0) cs_start_channel_join(cs, node_id);
} else {
/* topo_group_invite_to_channel: прямое ncd-подключение к connection-узлу БЕЗ INVITE_INFO.
Реальный join-протокол (JOIN_INFO_REQ → … → JOIN_READY) запускается по cs_on_conn_up. */
/* Транспорт принадлежит join-протоколу до его результата, а не будущей BGP-группе.
JOIN_INFO_REQ → … → JOIN_READY запускается по cs_on_conn_up. */
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "channel invite: connecting to 0x%016llx via %d addresses",
(unsigned long long)node_id, inv->addr_count);
int r = topo_group_invite_to_channel(inst, channel_id, ni, node_id);
int r = cs_join_hold_transport(cs, node_id, ni);
if (r < 0)
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "channel invite: FAILED — topo_group_invite_to_channel error=%d", r);
}
@@ -891,8 +1022,9 @@ void chat_sync_connect_from_invite(struct UTUN_INSTANCE* inst, uint64_t channel_
(long long)channel_id, (long long)node_id, addr_count, password ? "yes" : "no");
#endif
#ifndef __ANDROID__
uint64_t pubkey_prefix; memcpy(&pubkey_prefix, pubkey_bin, sizeof(pubkey_prefix));
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite start ch=%llu node=0x%016llx pubkey=%016llx... addrs=%d pass=%s",
CS_ID, channel_id, node_id, *(const uint64_t*)pubkey_bin, addr_count, password && password[0] ? "yes" : "no");
CS_ID, (unsigned long long)channel_id, (unsigned long long)node_id, (unsigned long long)pubkey_prefix, addr_count, password && password[0] ? "yes" : "no");
#endif
struct cm_invite_wrap* w = u_malloc(sizeof(struct cm_invite_wrap));
@@ -924,6 +1056,7 @@ void chat_sync_join_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint6
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
if (cs_join_hold_transport(cs, target_node_id, NULL) < 0) return;
cs_start_channel_join(cs, target_node_id);
cs_on_peer_status_changed(cs, target_node_id, 1);
}
@@ -1007,28 +1140,7 @@ static void cs_handle_join_info_req(struct chat_sync* cs, uint64_t peer,
return;
}
/* 2. пароль (если настроен на сервере) */
char req_password[128] = "";
if (len >= 10) {
uint8_t plen2 = pl[9];
if (plen2 > 0 && plen2 <= len - 10) {
if (plen2 > 127) plen2 = 127;
memcpy(req_password, pl + 10, plen2); req_password[plen2] = '\0';
}
}
{
struct global_config *gcfg = &cs->inst->config->global;
if (gcfg->chatserver_enabled && gcfg->chatserver_join_password[0]) {
if (strcmp(req_password, gcfg->chatserver_join_password) != 0) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_REQ wrong password ch=%s from=%016llx", CS_ID, ch_id, (unsigned long long)peer);
uint8_t err[2] = { CS_MSG_ERROR, 0x01 };
cs_send(cs, ch_id, peer, err, 2);
return;
}
}
}
/* 3. инфо канала */
/* 2. инфо канала */
char name[128]; uint64_t owner; uint8_t ch_x25519[32], ch_ed[32], ch_sig[64];
if (topo_node_sqlite_channel_get(cs->inst->topo_sqlite_db,
ch_id, name, (int)sizeof(name), &owner, ch_x25519, ch_ed, ch_sig) != 0) {
@@ -1116,9 +1228,11 @@ static void cs_handle_join_info_resp(struct chat_sync* cs, uint64_t peer,
uint64_t c_signed_by; memcpy(&c_signed_by, p, 8); p += 8;
const uint8_t* c_signature = p; p += 64;
uint64_t ch_x_prefix, ch_ed_prefix;
memcpy(&ch_x_prefix, ch_x25519, sizeof(ch_x_prefix)); memcpy(&ch_ed_prefix, ch_ed, sizeof(ch_ed_prefix));
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP from=%016llx owner=%016llx ch_x=%016llx ch_ed=%016llx name=%s conn_node=%016llx",
CS_ID, (unsigned long long)peer, (unsigned long long)owner,
*(const uint64_t*)ch_x25519, *(const uint64_t*)ch_ed, name, (unsigned long long)c_node_id);
(unsigned long long)ch_x_prefix, (unsigned long long)ch_ed_prefix, name, (unsigned long long)c_node_id);
/* verify channel signature */
uint8_t vmsg[1024]; size_t vlen = 0;
@@ -1282,13 +1396,8 @@ static void cs_handle_join_request(struct chat_sync* cs, uint64_t peer,
return;
}
/* валидный ключ → сразу добавляем узел в группу для BGP sync */
{
uint64_t gid = strtoull(ch_id, NULL, 10);
struct TOPO_GROUP* g = topo_groups_find(cs->inst->topo_groups, gid);
if (g && g->group_type == TOPO_GROUP_TYPE_CHAT)
topo_group_new_conn(g, jconn);
}
/* Ключ разрешает протокол добавления. BGP начнётся после сохранения мембера,
локально либо через Merkle; существующий транспорт обслуживает JOIN_REQUEST. */
/* добавить в pending (ждут JOIN_READY по приходу рекорда через merkle) */
cs_pending_add(cs, m.node_id, ch_id, jconn);
@@ -1301,6 +1410,14 @@ static void cs_handle_join_request(struct chat_sync* cs, uint64_t peer,
отправляем JOIN_READY напрямую, если мембер реально добавлен. */
if (topo_node_sqlite_member_in_channel(cs->inst->topo_sqlite_db, ch_id, m.node_id))
cs_send_join_ready(cs, ch_id, m.node_id);
/* join завершён — снять pending-состояние инвайтера и таймер ожидания */
if (cs->pending_invite_ch_id == channel_id && cs->pending_invite_node_id == m.node_id) {
if (cs->invite_send_timer) { uasync_cancel_timeout(cs->inst->ua, cs->invite_send_timer); cs->invite_send_timer = NULL; }
cs->pending_invite_ch_id = 0;
cs->pending_invite_node_id = 0;
cs->pending_invite_is_inviter = 0;
cs_join_release_transport(cs);
}
} else {
chat_join_forward_request(cs->inst, channel_id, inviter, join_key, &m);
}
@@ -1324,7 +1441,7 @@ static void cs_handle_join_ready(struct chat_sync* cs, uint64_t peer,
struct TOPO_GROUP* g = topo_groups_find(cs->inst->topo_groups, group_id);
if (g && g->group_type == TOPO_GROUP_TYPE_CHAT) {
struct ETCP_CONN* c = cs_find_conn_for_node(cs->inst, peer);
if (c) topo_group_new_conn(g, c);
if (c) topo_group_connect_node_once(g, c->peer_node_id);
}
}
chat_core_ensure_channel_ready(cs->inst, ch_id);
@@ -1341,6 +1458,7 @@ static void cs_handle_join_ready(struct chat_sync* cs, uint64_t peer,
chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, cevt, 20);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_READY invite success ch=%s peer=%016llx", CS_ID, ch_id, (unsigned long long)peer);
cs->pending_invite_node_id = 0; cs->pending_invite_ch_id = 0; cs->pending_join_key = 0;
cs_join_release_transport(cs);
}
}
@@ -1371,6 +1489,10 @@ static void cs_handle_channel_invite(struct chat_sync* cs, uint64_t peer,
/* skip inviter_update_sig/ts/userinfo for now — verify channel sig only */
p += 64; uint64_t inviter_update_ts; memcpy(&inviter_update_ts, p, 8); p += 8;
uint8_t userinfo_len = *p++; p += userinfo_len;
/* join-ключ (зарегистрирован инвайтером) — хвостовые 8 байт */
uint64_t join_key = 0;
if (p + 8 > pl + len) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE truncated (no join_key) from=%016llx", CS_ID, (unsigned long long)peer); return; }
memcpy(&join_key, p, 8); p += 8;
/* Verify channel signature */
uint8_t vmsg[1024]; size_t vlen = 0;
@@ -1405,16 +1527,30 @@ static void cs_handle_channel_invite(struct chat_sync* cs, uint64_t peer,
/* Save channel and start join handshake */
int cprc = topo_node_sqlite_channel_put(cs->inst->topo_sqlite_db,
ch_id, name, owner, x25519, NULL, ed_pub, NULL, ch_sig);
uint64_t ch_x_prefix; memcpy(&ch_x_prefix, x25519, sizeof(ch_x_prefix));
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE channel_put rc=%d ch=%s ch_x25519=%016llx",
CS_ID, cprc, ch_id, *(const uint64_t*)x25519);
CS_ID, cprc, ch_id, (unsigned long long)ch_x_prefix);
chat_core_ensure_channel_ready(cs->inst, ch_id);
/* Initiate join: connect back and send CHANNEL_INFO_REQ */
/* Initiate join: connect back and send JOIN_INFO_REQ с зарегистрированным ключом */
cs->pending_join_key = join_key;
chat_sync_join_channel(cs->inst, ch_id, peer);
return;
}
/* policy == ask — post event to GUI */
{
/* сохранить join-ключ, чтобы принять инвайт позже через chat_sync_accept_invite */
uint64_t ch_num = strtoull(ch_id, NULL, 10);
int slot = -1;
for (int i = 0; i < cs->pending_inv_in_count; i++)
if (cs->pending_inv_in_ch_id[i] == ch_num && cs->pending_inv_in_node_id[i] == peer) { slot = i; break; }
if (slot < 0 && cs->pending_inv_in_count < 4) slot = cs->pending_inv_in_count++;
if (slot >= 0) {
cs->pending_inv_in_ch_id[slot] = ch_num;
cs->pending_inv_in_node_id[slot] = peer;
cs->pending_inv_in_key[slot] = join_key;
}
size_t inv_name_len = 0; char inv_name[128] = "";
{ sqlite3* vdb = cs->inst->topo_sqlite_db;
sqlite3_stmt* st = NULL;
@@ -1457,6 +1593,15 @@ static int cs_send_channel_invite(struct chat_sync* cs, struct UTUN_INSTANCE* in
int ulen = member_sync_build_update_msg(join_sig, upd_ts, juser, umsg, (int)sizeof(umsg));
if (ulen >= 0) cs_ed25519_sign(inst->my_ed25519_privkey, umsg, (size_t)ulen, upd_sig);
}
/* join-ключ: генерируем и регистрируем локально (инвайтер == connection-узел),
чтобы джойнер смог пройти JOIN_INFO_REQ с валидным ключом. */
uint64_t join_key = 0;
if (RAND_bytes((uint8_t*)&join_key, sizeof(join_key)) != 1 || join_key == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_key generation failed ch=%s", CS_ID, ch_id);
return -1;
}
if (chat_join_store_local_key(inst, strtoull(ch_id, NULL, 10), join_key) != 0) return -1;
uint8_t buf[1024]; size_t boff = 0;
buf[boff++] = CS_MSG_CHANNEL_INVITE;
uint8_t cnl = (uint8_t)strlen(ch_name);
@@ -1476,9 +1621,10 @@ static int cs_send_channel_invite(struct chat_sync* cs, struct UTUN_INSTANCE* in
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", inst->name[0] ? inst->name : "");
uint8_t il = (uint8_t)strlen(juser);
buf[boff++] = il; memcpy(buf + boff, juser, il); boff += il;
memcpy(buf + boff, &join_key, 8); boff += 8;
int rc = cs_send(cs, ch_id, target_node_id, buf, boff);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: sent CHANNEL_INVITE to 0x%016llx ch=%s rc=%d",
CS_ID, (unsigned long long)target_node_id, ch_id, rc);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: sent CHANNEL_INVITE to 0x%016llx ch=%s key=%016llx rc=%d",
CS_ID, (unsigned long long)target_node_id, ch_id, (unsigned long long)join_key, rc);
return rc;
}
@@ -1521,7 +1667,7 @@ void chat_sync_invite_to_channel(struct UTUN_INSTANCE* inst, const char* ch_id,
}
}
/* Адреса грузятся из node_addresses в БД внутри node_conn_direct_open */
int r = topo_group_invite_to_channel(inst, channel_id, NULL, target_node_id);
int r = cs_join_hold_transport(cs, target_node_id, NULL);
if (r < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel topo_group_invite_to_channel failed rc=%d", CS_ID, r);
}
@@ -1537,6 +1683,8 @@ void chat_sync_invite_to_channel(struct UTUN_INSTANCE* inst, const char* ch_id,
}
/* Already connected — send invite now, wait for joiner to respond */
if (cs_join_hold_transport(cs, target_node_id, NULL) < 0) return;
uasync_cancel_timeout(inst->ua, cs->join_transport_timer); cs->join_transport_timer = NULL;
cs_send_channel_invite(cs, inst, ch_id, target_node_id, ch_name, owner, ch_x25519, ch_ed, ch_sig);
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
@@ -1580,6 +1728,8 @@ static void cs_invite_trampoline(void* arg) {
char ch_name[128]; uint8_t ch_x2[32], ch_ed2[32], ch_sig[64]; uint64_t ch_owner;
if (topo_node_sqlite_channel_get(inst->topo_sqlite_db,
ch_id, ch_name, (int)sizeof(ch_name), &ch_owner, ch_x2, ch_ed2, ch_sig) == 0) {
if (cs_join_hold_transport(cs, target_node_id, NULL) < 0) { u_free(w); return; }
uasync_cancel_timeout(inst->ua, cs->join_transport_timer); cs->join_transport_timer = NULL;
cs_send_channel_invite(cs, inst, ch_id, target_node_id, ch_name, ch_owner, ch_x2, ch_ed2, ch_sig);
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
@@ -1636,7 +1786,7 @@ static void cs_invite_trampoline(void* arg) {
cs->pending_invite_node_id = target_node_id;
cs->pending_invite_is_inviter = 1;
int r = topo_group_invite_to_channel(inst, channel_id, ni, target_node_id);
int r = cs_join_hold_transport(cs, target_node_id, ni);
if (r < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs topo_group_invite_to_channel failed rc=%d", CS_ID, r);
cs->pending_invite_node_id = 0; cs->pending_invite_ch_id = 0;
@@ -1684,16 +1834,31 @@ void chat_sync_invite_to_channel_with_addrs(
/* ─── accept/deny incoming invite ─── */
void chat_sync_accept_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t inviter_node_id) {
if (!cs_of(inst) || !inst) return;
struct chat_sync* cs = cs_of(inst);
if (!cs || !inst) return;
char ch_id[64]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)channel_id);
if (channel_id == 0 || inviter_node_id == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: accept_invite invalid params ch=%llu node=%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id);
return;
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: accept_invite ch=%llu inviter=0x%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id);
/* достать join-ключ, сохранённый при приёме CHANNEL_INVITE (ask) */
uint64_t join_key = 0;
for (int i = 0; i < cs->pending_inv_in_count; i++) {
if (cs->pending_inv_in_ch_id[i] == channel_id && cs->pending_inv_in_node_id[i] == inviter_node_id) {
join_key = cs->pending_inv_in_key[i];
break;
}
}
if (join_key == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: accept_invite no join_key for ch=%llu inviter=0x%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id);
return;
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: accept_invite ch=%llu inviter=0x%016llx key=%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id, (unsigned long long)join_key);
chat_core_ensure_channel_ready(inst, ch_id);
cs->pending_join_key = join_key;
chat_sync_join_channel(inst, ch_id, inviter_node_id);
}
+22 -31
View File
@@ -1,25 +1,10 @@
/*
* chat_sync.h — оркестратор синхронизации чата по ETCP
*
* При поднятии / разрыве ETCP-соединений:
* - обновляет online-статус пиров в БД и GUI
* - запускает member_sync (синхронизацию участников каналов) через merkle_sync
* - управляет кешем каналов (периодический refresh из БД, 30s)
*
* Обслуживает протокол входа в канал (join) между джойнером, connection-узлом и инвайтером:
* JOIN_INFO_REQ → JOIN_INFO_RESP → JOIN_REQUEST → (форвард инвайтеру) → JOIN_READY.
* Полное описание flow и дерева приглашений — в chat_join.h.
*
* Обрабатывает invite-ссылки (chat_sync_connect_from_invite):
* - сохраняет pubkey, join_key и адреса 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) — вызывается при завершении
* chat_sync — связь чат-сервиса с транспортом и группами.
* Отслеживает пиров, запускает member_sync, поддерживает кеш каналов и статус в GUI.
* Вход по ссылке держит отдельный NCD handle до результата join, а не только до UP.
* JOIN_INFO_REQ → JOIN_INFO_RESP → JOIN_REQUEST → JOIN_READY; правила — в chat_join.h.
* Жизненным циклом управляет chat_service_start/stop. Вызовы — в потоке uasync,
* кроме явно отмеченной обёртки, которая копирует аргументы и постит работу в этот поток.
*/
#ifndef CHAT_SYNC_H
@@ -35,9 +20,8 @@ extern "C" {
struct UTUN_INSTANCE;
struct UASYNC;
/* etcp service ID (direct P2P) */
#define ETCP_RT_ID_CHAT_SYNC 0x30
#define ETCP_RT_ID_MEMBER_SYNC 0x31
/* etcp service IDs (direct P2P): ETCP_RT_ID_CHAT_SYNC / ETCP_RT_ID_MEMBER_SYNC
* (etcp_bind, см. etcp_api.h) */
/* Message types */
#define CS_MSG_INIT_SYNC 0x01
@@ -52,6 +36,7 @@ struct UASYNC;
#define CS_MSG_JOIN_INFO_RESP 0x11 /* connection → joiner: channel info + connection's member record */
#define CS_MSG_JOIN_REQUEST 0x12 /* joiner → connection: {join_key:8, member...} */
#define CS_MSG_JOIN_READY 0x13 /* connection → joiner: {group_id:8} */
#define CS_MSG_SOUND_SETTING 0x14 /* peer → server: {sound_on:1} — per-chat play sound on message */
/* Protocol constants */
#define CS_SEND_DATA_MAX 32
@@ -65,15 +50,17 @@ struct UASYNC;
/* ── Public API ── */
/* Создать контекст, обработчики join и member_sync. 0/-1; повтор допустим. */
int chat_sync_init(struct UTUN_INSTANCE* inst);
/* Отменить ожидания join, синхронизацию и таймеры; не удаляет данные каналов. */
void chat_sync_destroy(struct UTUN_INSTANCE* inst);
/* Прямое ETCP-подключение к узлу (pubkey+адреса из БД) */
/* Сейчас пустая функция. Для подключения использовать chat_core_connect_node с каналом. */
void chat_sync_connect_node(struct UTUN_INSTANCE* inst, uint64_t node_id);
/* Подключиться к пиру по данным invite-ссылки (вызывается из uasync-потока).
join_key — 64-битный ключ входа из ссылки (регистрируется инвайтером на connection-узле).
password может быть NULL — тогда отправляется без пароля (совместимость с v1) */
password может быть NULL — тогда передаётся пустой пароль. */
void chat_sync_connect_from_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t node_id,
const uint8_t* pubkey_bin,
const uint8_t* addrs_data, int addr_count,
@@ -82,7 +69,7 @@ void chat_sync_connect_from_invite(struct UTUN_INSTANCE* inst, uint64_t channel_
/* Пригласить target_node_id в канал ch_id с адресами из invite-ссылки (inviter → joiner).
Принимает pubkey и адреса целевого узла. Если соединения ещё нет — строит TOPO_NODE
с адресами, запускает conn_mgr для подключения, и по conn_up отправляет CS_MSG_CHANNEL_INVITE.
с адресами, удерживает NCD для join, и по UP отправляет CS_MSG_CHANNEL_INVITE.
Если соединение уже есть — отправляет CHANNEL_INVITE сразу.
Копирует pubkey_bin и addrs_data (вызывающий может освободить после возврата).
Вызов из любого потока — внутри постит в uasync. */
@@ -95,7 +82,7 @@ void chat_sync_invite_to_channel_with_addrs(
Отправляет CS_MSG_CHANNEL_INVITE с полной информацией о канале и своих адресах */
void chat_sync_invite_to_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id);
/* Принять входящее приглашение — продолжаем join-протокол (CHANNEL_JOIN → WELCOME) */
/* Принять входящее приглашение — продолжить протокол из chat_join.h. */
void chat_sync_accept_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t inviter_node_id);
/* Отклонить входящее приглашение */
@@ -103,18 +90,22 @@ void chat_sync_deny_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint
/* Присоединиться к каналу через уже подключённый узел (uasync-поток).
Соединение с target_node_id должно быть установлено (links_up && initialized).
Протокол: CHANNEL_INFO_REQ → CHANNEL_INFO_RESP → CHANNEL_JOIN → WELCOME.
Канал (DB + TOPO_GROUP) создаётся локально при получении CHANNEL_INFO_RESP. */
Протокол: JOIN_INFO_REQ → JOIN_INFO_RESP → JOIN_REQUEST → JOIN_READY (chat_join.h).
Канал (DB + TOPO_GROUP) создаётся локально при получении JOIN_INFO_RESP;
это само по себе не даёт членства на других узлах. */
void chat_sync_join_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id);
/* Перезапустить групповые подключения для каналов без активных соединений.
Вызывается из auto_socket при появлении/изменении сетевой связности. */
void chat_sync_retry_channels_on_socket_change(struct UTUN_INSTANCE* inst);
/* Немедленно убрать канал из кеша g_cs->channels (локальное удаление канала),
/* Немедленно убрать канал из кеша chat_sync (локальное удаление канала),
* чтобы cs_on_conn_status не запускал member_sync для удалённого ns до следующего refresh. */
void chat_sync_on_channel_deleted(struct UTUN_INSTANCE* inst, const char* ch_id);
/* Разослать всем подключённым пирам канала смену per-chat настройки «play sound on message». */
void chat_sync_broadcast_sound_setting(struct UTUN_INSTANCE* inst, const char* ch_id, int on);
#ifdef __cplusplus
}
#endif
+1 -1
View File
@@ -345,8 +345,8 @@ static void wh_work_fn(void* raw) {
}
static void wh_done_fn(void* raw, int err) {
(void)err;
struct wh_work_ctx* w = (struct wh_work_ctx*)raw;
if (err) w->err = err;
g_processing = 0;
+73 -29
View File
@@ -25,6 +25,7 @@
// ---- Forward declarations ----
struct DB_SYNC;
struct DB_SYNC_INSTANCE;
struct SI_PEER;
static void db_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry);
static void db_sync_on_conn_status(struct ETCP_CONN* conn, int status, void* arg);
@@ -38,6 +39,7 @@ static int db_sync_send_gid(struct DB_SYNC* db, uint64_t node_id, uint64_t grou
static void db_sync_initiate_sync(struct DB_SYNC_INSTANCE* si, uint64_t peer_node_id);
static int si_send_data_batch(struct DB_SYNC_INSTANCE* si, uint64_t dst, uint32_t from, uint32_t count, uint32_t vp, uint32_t want_from);
static void si_delivery_update(struct DB_SYNC_INSTANCE* si, uint64_t ts, uint64_t author, uint64_t peer_id);
static int db_sync_peer_throttled(struct DB_SYNC* db, struct SI_PEER* p);
// ============================================================
// Structures
@@ -51,6 +53,7 @@ struct SI_PEER {
uint8_t sync_state;
uint64_t sync_start_tb;
uint32_t retry_count;
uint8_t sound_on; /* 1 = пиру важен этот чат (play sound on message), не троттлим */
};
struct DB_SYNC_INSTANCE {
@@ -237,6 +240,11 @@ struct DB_SYNC_INSTANCE* db_sync_instance_find(struct UTUN_INSTANCE* inst, uint6
return db_instance_find(inst->db_sync, group_id);
}
uint64_t db_sync_instance_group_id(struct DB_SYNC_INSTANCE* si)
{
return si ? si->group_id : 0;
}
// Выделяет новый инстанс в куче (стабильный указатель) и регистрирует его в массиве
static struct DB_SYNC_INSTANCE* db_instance_alloc(struct DB_SYNC* db)
{
@@ -306,6 +314,7 @@ static struct SI_PEER* si_peer_add(struct DB_SYNC_INSTANCE* si, uint64_t node_id
p = &si->peers[si->peer_count++];
memset(p, 0, sizeof(*p));
p->node_id = node_id;
p->sound_on = 1;
return p;
}
@@ -560,9 +569,10 @@ static int db_verify_author_sig(struct DB_SYNC_INSTANCE* si,
memcpy(msg + off, &ts, 8); off += 8;
if (jlen > 0) { memcpy(msg + off, json, jlen); off += jlen; }
if (sc_ed25519_verify(pubkey, msg, off, sig) != SC_OK) {
uint64_t key_prefix, sig_prefix; memcpy(&key_prefix, pubkey, 8); memcpy(&sig_prefix, sig, 8);
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC,
"author_sig VERIFY FAIL author=%016llx ed_pubkey=%016llx... msg_len=%zu sig=%016llx... — discarding as forgery",
(unsigned long long)author_node_id, *(uint64_t*)pubkey, off, *(uint64_t*)sig);
(unsigned long long)author_node_id, (unsigned long long)key_prefix, off, (unsigned long long)sig_prefix);
return -1;
}
return 0;
@@ -646,12 +656,12 @@ static int db_record_insert_cascade(struct DB_SYNC_INSTANCE* si,
uint64_t rid, uint64_t rts, uint64_t rauthor,
const char* rdata, uint32_t rdlen,
const uint8_t* rsig, uint64_t source_peer,
const char* local_attrs)
const char* local_attrs, int initial_sync)
{
int ret = db_record_insert(si, rid, rts, rauthor, rdata, rdlen, rsig, local_attrs);
if (ret != 0) return ret;
if (si->on_insert) si->on_insert(si, rts, rdata, rdlen, rauthor, si->on_insert_arg);
if (si->on_insert) si->on_insert(si, rts, rdata, rdlen, rauthor, initial_sync, si->on_insert_arg);
uint8_t pbuf[4096]; uint32_t poff = 1; pbuf[0] = DB_MSG_PUSH;
memcpy(pbuf + poff, &rid, 8); poff += 8;
@@ -667,6 +677,7 @@ static int db_record_insert_cascade(struct DB_SYNC_INSTANCE* si,
for (int i = 0; i < si->peer_count; i++) {
uint64_t pid = si->peers[i].node_id;
if (si->peers[i].sync_state >= 1 && pid != source_peer && pid != my_id) {
if (db_sync_peer_throttled(si->db_sync, &si->peers[i])) continue; /* спящий пир в «тихом» чате — не пушим */
if (db_sync_send(si, pid, pbuf, poff) >= 0)
{ si_delivery_update(si, rts, rauthor, pid); push_count++; }
}
@@ -700,10 +711,11 @@ static int db_sync_send_gid(struct DB_SYNC* db, uint64_t node_id, uint64_t group
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync: send DROP — no conn to %04llX (group=%016llx, type=%02x)",
(unsigned long long)(node_id >> 16), group_id, plen > 0 ? payload[0] : 0,
(void*)conn, conn ? conn->links_up : -1);
queue_entry_free(entry);
queue_dgram_free(entry); queue_entry_free(entry);
return -1;
}
int ret = etcp_send(conn, entry);
if (ret != 0) { queue_dgram_free(entry); queue_entry_free(entry); }
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "sync: send OK → etcp_send ret=%d conn=%s l_up=%d init=%d q=%p",
ret, conn->log_name, conn->links_up, conn->initialized, (void*)conn->send_input_q);
return ret;
@@ -797,7 +809,7 @@ static void db_handle_init_sync(struct DB_SYNC_INSTANCE* si, uint64_t src, const
{
if (len < 4) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "INIT_SYNC too short %zu from %016llx", len, (unsigned long long)src); return; }
db_sync_flush_recalc(si);
uint32_t pc = *(uint32_t*)p;
uint32_t pc; memcpy(&pc, p, 4);
uint32_t mc = db_count(si);
uint32_t tp = (pc < mc ? pc : mc);
if (tp > 0) tp--;
@@ -850,7 +862,7 @@ static void db_handle_init_resp(struct DB_SYNC_INSTANCE* si, uint64_t src, const
{
if (len < 14) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "INIT_RESP too short %zu from %016llx", len, (unsigned long long)src); return; }
db_sync_flush_recalc(si);
uint32_t tp = *(uint32_t*)p;
uint32_t tp; memcpy(&tp, p, 4);
uint64_t peer_ch8; memcpy(&peer_ch8, p + 4, 8);
uint8_t has_tail = p[12];
uint8_t sc = p[13];
@@ -907,7 +919,7 @@ static void db_handle_init_resp(struct DB_SYNC_INSTANCE* si, uint64_t src, const
// Mismatch: find first divergent position
uint32_t fm = tp;
for (int i = 0; i < sc && spr + 12 <= p + len; i++) {
uint32_t pos = *(uint32_t*)spr; uint64_t pch8; memcpy(&pch8, spr + 4, 8); spr += 12;
uint32_t pos; memcpy(&pos, spr, 4); uint64_t pch8; memcpy(&pch8, spr + 4, 8); spr += 12;
uint64_t mch8;
if (db_chain_hash8_at(si, pos, &mch8) == 0) {
if (mch8 != pch8 && pos < fm) fm = pos;
@@ -939,7 +951,7 @@ static int si_send_data_batch(struct DB_SYNC_INSTANCE* si, uint64_t dst, uint32_
buf[off++] = DB_MSG_SEND_DATA;
memcpy(buf + off, &from, 4); off += 4;
uint16_t rc = 0;
uint16_t* rcp = (uint16_t*)(buf + off); off += 2;
uint8_t* rcp = buf + off; off += 2;
memcpy(buf + off, &vp, 4); off += 4;
memcpy(buf + off, &want_from, 4); off += 4; // NEW: request peer's data from this position, DB_WANT_FROM_NONE=none
@@ -988,7 +1000,7 @@ static int si_send_data_batch(struct DB_SYNC_INSTANCE* si, uint64_t dst, uint32_
SI_SHRT(si), (unsigned long long)(dst >> 16), from, count);
}
*rcp = rc;
memcpy(rcp, &rc, 2);
db_sync_send(si, dst, buf, off);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] → SEND_DATA: %u recs from=%u want=%u vp=%u",
SI_SHRT(si), (unsigned long long)(dst >> 16), rc, from, want_from, vp);
@@ -1005,10 +1017,10 @@ static int si_parse_record(const uint8_t** pp, const uint8_t* end,
const uint8_t** rsig, int* rsiglen)
{
if (*pp + 28 > end) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "si_parse_record: truncated header, need 28 have %td", end - *pp); return -1; }
*rid = *(uint64_t*)*pp; *pp += 8;
*rts = *(uint64_t*)*pp; *pp += 8;
*rauthor = *(uint64_t*)*pp; *pp += 8;
*rdlen = *(uint32_t*)*pp; *pp += 4;
memcpy(rid, *pp, 8); *pp += 8;
memcpy(rts, *pp, 8); *pp += 8;
memcpy(rauthor, *pp, 8); *pp += 8;
memcpy(rdlen, *pp, 4); *pp += 4;
if (*pp + *rdlen > end) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "si_parse_record: data overrun dlen=%u have=%td", *rdlen, end - *pp); return -1; }
*rdata = (*rdlen > 0) ? *pp : NULL;
@@ -1033,10 +1045,10 @@ static void db_handle_send_data(struct DB_SYNC_INSTANCE* si, uint64_t src, const
{
if (len < 14) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← SEND_DATA: truncated len=%zu (need >=14)", SI_SHRT(si), (unsigned long long)(src >> 16), len); return; }
db_sync_flush_recalc(si);
uint32_t from = *(uint32_t*)p;
uint16_t count = *(uint16_t*)(p + 4);
uint32_t vp = *(uint32_t*)(p + 6);
uint32_t want_from = *(uint32_t*)(p + 10);
uint32_t from; memcpy(&from, p, 4);
uint16_t count; memcpy(&count, p + 4, 2);
uint32_t vp; memcpy(&vp, p + 6, 4);
uint32_t want_from; memcpy(&want_from, p + 10, 4);
struct SI_PEER* sp = si_peer_find(si, src);
@@ -1045,7 +1057,7 @@ static void db_handle_send_data(struct DB_SYNC_INSTANCE* si, uint64_t src, const
for (uint16_t i = 0; i < count && i < 32; i++) {
uint64_t rid, rts, rauthor; uint32_t rdlen; const uint8_t* rdata, *rsig; int rsiglen;
if (si_parse_record(&ptr, p + len, &rid, &rts, &rauthor, &rdlen, &rdata, &rsig, &rsiglen) != 0) break;
int ret = db_record_insert_cascade(si, rid, rts, rauthor, (const char*)rdata, rdlen, rsig, src, NULL);
int ret = db_record_insert_cascade(si, rid, rts, rauthor, (const char*)rdata, rdlen, rsig, src, NULL, 1);
if (ret >= 0) received++;
if (ret == 1) duplicates++;
if (ret == -2) bad_sigs++;
@@ -1099,7 +1111,7 @@ static void db_handle_sync_done(struct DB_SYNC_INSTANCE* si, uint64_t src, const
{
if (len < 12) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "SYNC_DONE too short %zu from %016llx", len, (unsigned long long)src); return; }
db_sync_flush_recalc(si);
uint32_t pc = *(uint32_t*)p;
uint32_t pc; memcpy(&pc, p, 4);
uint64_t pch8; memcpy(&pch8, p + 4, 8);
uint32_t mc = db_count(si);
uint64_t mch8 = 0; if (mc > 0) db_chain_hash8_at(si, mc - 1, &mch8);
@@ -1141,8 +1153,8 @@ static void db_handle_sync_done(struct DB_SYNC_INSTANCE* si, uint64_t src, const
static void db_handle_ack_push(struct DB_SYNC_INSTANCE* si, uint64_t src, const uint8_t* p, size_t len)
{
if (len < 16) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← PUSH ACK: truncated len=%zu (need >=16)", SI_SHRT(si), (unsigned long long)(src >> 16), len); return; }
uint64_t ts = *(uint64_t*)p;
uint64_t author = *(uint64_t*)(p + 8);
uint64_t ts; memcpy(&ts, p, 8);
uint64_t author; memcpy(&author, p + 8, 8);
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "from=%016llx ts=%llu author=%016llx", (unsigned long long)src, (unsigned long long)ts, (unsigned long long)author);
sqlite3_stmt* stmt;
@@ -1177,7 +1189,7 @@ static void db_handle_push(struct DB_SYNC_INSTANCE* si, uint64_t src, const uint
if (si_parse_record(&ptr, p + len, &rid, &rts, &rauthor, &rdlen, &rdata, &rsig, &rsiglen) != 0) return;
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "from=%016llx id=%llu author=%016llx ts=%llu len=%u", (unsigned long long)src, (unsigned long long)rid, (unsigned long long)rauthor, (unsigned long long)rts, rdlen);
int ret = db_record_insert_cascade(si, rid, rts, rauthor, (const char*)rdata, rdlen, rsig, src, NULL);
int ret = db_record_insert_cascade(si, rid, rts, rauthor, (const char*)rdata, rdlen, rsig, src, NULL, 0);
if (ret == 0) {
uint32_t ins_pos = si_find_pos(si, rts, rsig);
uint32_t total = db_count(si);
@@ -1213,7 +1225,7 @@ static void db_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry)
if (!db || !db->enabled) { queue_dgram_free(entry); queue_entry_free(entry); return; }
uint64_t src = conn ? conn->peer_node_id : 0;
uint64_t group_id = be64toh(*(uint64_t*)(entry->dgram + 1));
uint64_t group_id; memcpy(&group_id, entry->dgram + 1, 8); group_id = be64toh(group_id);
uint8_t type = entry->dgram[9];
const uint8_t* payload = entry->dgram + 10;
size_t plen = entry->len - 10;
@@ -1469,6 +1481,22 @@ void db_sync_peer_set_state(struct DB_SYNC_INSTANCE* si, uint64_t node_id, uint8
if (p) p->sync_state = state;
}
// Задать per-(chat,peer) флаг «play sound on message» (троттлинг: sound_off + спящий пир → пропуск).
void db_sync_set_peer_sound(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t peer_node_id, int sound_on)
{
if (!inst || !inst->db_sync) return;
struct DB_SYNC* db = inst->db_sync;
struct DB_SYNC_INSTANCE* si = db_instance_find(db, group_id);
if (!si) return; /* чат ещё не создан у нас — настройка не критична */
struct SI_PEER* p = si_peer_add(si, peer_node_id);
if (p && p->sound_on != (uint8_t)(sound_on ? 1 : 0)) {
p->sound_on = sound_on ? 1 : 0;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s] peer=%016llx sound_on=%d (throttle=%s)",
SI_SHRT(si), (unsigned long long)peer_node_id, p->sound_on,
p->sound_on ? "off" : "on");
}
}
// Гейт авто-синхронизации: пока gated — инстанс не запускает sync автоматически
// (ждёт member_sync/pubkeys). При снятии гейта запускает sync к пирам в sync_state==0.
void db_sync_instance_set_gated(struct DB_SYNC_INSTANCE* si, int gated)
@@ -1546,6 +1574,15 @@ static void db_sync_resume_peer_check(struct DB_SYNC* db)
// Периодическая проверка: запускает синхронизацию для пиров в состоянии sync_state==0,
// детектирует таймауты синхронизации (DB_SYNC_SYNC_TIMEOUT секунд), логирует зависшие.
// Work-driven: перевзводится только пока есть пиры с sync_state != 2.
/* Троттлинг: пир в SLEEP-фазе И чат для него «тихий» (sound_on=0) — пропускаем. */
static int db_sync_peer_throttled(struct DB_SYNC* db, struct SI_PEER* p) {
if (!db || !p) return 0;
if (p->sound_on) return 0; /* чат пиру важен (play sound) — не троттлим */
struct ETCP_CONN* conn = instance_find_conn(db->inst, p->node_id);
return conn && conn->peer_sleep_phase;
}
static void db_sync_peer_check_cb(void* arg)
{
struct DB_SYNC* db = (struct DB_SYNC*)arg;
@@ -1560,13 +1597,13 @@ static void db_sync_peer_check_cb(void* arg)
#endif
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "instances=%d", db->instance_count);
struct TOPO_GROUP* g = topo_groups_get_default(db->inst->topo_groups);
int total_synced = 0, total_skipped = 0, any_peers = 0;
char launched_list[256] = "";
if (g) {
{
for (int i = 0; i < db->instance_count; i++) {
struct DB_SYNC_INSTANCE* si = db->instances[i];
struct TOPO_GROUP* g = topo_groups_find(db->inst->topo_groups, si->group_id);
if (!g) continue;
if (!si->enabled) continue;
if (si->sync_gated) continue;
@@ -1585,6 +1622,7 @@ static void db_sync_peer_check_cb(void* arg)
uint32_t min_pos = UINT32_MAX;
for (int j = 0; j < si->peer_count; j++) {
if (si->peers[j].sync_state == 0 && si->peers[j].synced_pos < min_pos) {
if (db_sync_peer_throttled(db, &si->peers[j])) continue; /* спящий пир в «тихом» чате — пропускаем */
min_pos = si->peers[j].synced_pos;
best = &si->peers[j];
}
@@ -1698,9 +1736,14 @@ int db_sync_init(struct UTUN_INSTANCE* inst)
return 0;
}
db->enabled = 1;
inst->db_sync = db;
return db_sync_enable(inst);
}
int db_sync_enable(struct UTUN_INSTANCE* inst) {
struct DB_SYNC* db = inst ? inst->db_sync : NULL;
if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "db_sync enable requires initialized core"); return -1; }
if (db->enabled) return 0;
const char* dp = inst->config->global.db_path;
if (inst->topo_sqlite_db) {
db->db = inst->topo_sqlite_db; db->shared_db = 1;
@@ -1711,10 +1754,11 @@ int db_sync_init(struct UTUN_INSTANCE* inst)
else snprintf(sp, sizeof(sp), "/tmp/utun_db_sync");
if (db_sqlite_open(db, sp) != 0) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "SQLite open failed, sync disabled");
db->enabled = 0;
return -1;
}
}
db->enabled = 1;
etcp_bind(inst, ETCP_RT_ID_DB_SYNC, db_sync_recv_cb);
etcp_add_conn_status_cbk(inst, db_sync_on_conn_status, NULL);
@@ -1880,7 +1924,7 @@ int db_sync_insert_signed(struct DB_SYNC_INSTANCE* si, const char* json_data, si
uint64_t id = si->next_id;
uint64_t author_node_id = si->db_sync->inst->node_id;
return db_record_insert_cascade(si, id, ts, author_node_id, json_data, (uint32_t)len, sig, author_node_id, local_attrs);
return db_record_insert_cascade(si, id, ts, author_node_id, json_data, (uint32_t)len, sig, author_node_id, local_attrs, 0);
}
// Возвращает количество записей в инстансе
+29 -49
View File
@@ -1,42 +1,10 @@
// db_sync.h — Distributed content-addressed table with SQLite + peer sync via etcp_api (direct P2P)
//
// Назначение: децентрализованная реплицируемая таблица JSON-записей между всеми узлами сети.
// Модуль поддерживает несколько независимых инстансов (таблиц), каждый идентифицируется парой (name, id).
//
// Каждая запись ОБЯЗАТЕЛЬНО содержит Ed25519-подпись автора.
// chain_hash[pos] = SHA256(chain_hash[pos-1] || id || timestamp || author || author_signature[64])
// Первичный ключ: (timestamp, author) — тот же автор в ту же ms = дубликат.
// Упорядочение: ORDER BY timestamp, author.
//
// Использование:
// 1. Включить в конфиге: db_sync_enabled = 1
// Опционально: db_sync_ttl = 86400 (по умолчанию)
// 2. db_sync_init() — вызывается автоматически при старте utun_instance
// 3. struct DB_SYNC_INSTANCE* si = db_sync_instance_add(inst, "chats", 1);
// Создаёт/регистрирует инстанс. При поднятии соединений автоматически запускает sync.
// 4. uint64_t ts = db_sync_next_timestamp(si);
// sig = Ed25519(ts || my_node_id || json_data)
// db_sync_insert_signed(si, json_data, len, sig, 64, ts) — вставить запись.
// sig=NULL → ошибка.
// 5. db_sync_count(si) — количество записей в локальной БД для этого инстанса
// 6. db_sync_instance_remove(si) — удалить инстанс (таблица БД не удаляется)
// 7. db_sync_destroy() — вызывается автоматически при завершении
//
// Синхронизация:
// - При поднятии ETCP-соединения с пиром для каждого инстанса запускается полная синхронизация
// - Каждое сообщение содержит group_id (uint64, binary), что позволяет маршрутизировать сообщения к нужному инстансу на приёмной стороне
// - Если пир не имеет инстанса с таким хешем — возвращает DB_MSG_ERROR(0x01)
// - Если пир добавляет инстанс позже — сам инициирует sync в нашу сторону
// - Протокол: сравнение chain_hash для поиска расхождений (7 типов сообщений)
// - Новые записи немедленно рассылаются подключённым пирам через PUSH
//
// Wire-формат записи (SEND_DATA/PUSH): [id:8][ts:8][author:8][dlen:4][data][sig_len:1=64][sig:64]
//
// Нюансы:
// - Записи не редактируются и не удаляются явно — только TTL-очистка (per-instance)
// - Дубликаты определяются по (timestamp, author)
// - БД хранится в SQLite, путь: <db_path>/sync
// - Синхронизация идёт через etcp_api (service ID 0x20), только прямые P2P соединения
/* db_sync — репликация подписанных записей SQLite по прямым ETCP-соединениям.
* Каждый DB_SYNC_INSTANCE связывает таблицу с group_id. Различия находятся по
* цепочке хешей; новые записи рассылаются PUSH. Ключ и порядок записей:
* (timestamp, author_signature). Подпись автора покрывает timestamp || data.
* Ядро создаёт модуль, чат включает его и регистрирует таблицы каналов.
* Обычно используется общая SQLite ядра; remove отключает таблицу от обмена,
* сохраняя данные. Все вызовы и callbacks выполняются в потоке uasync. */
#ifndef DB_SYNC_H
#define DB_SYNC_H
@@ -50,8 +18,7 @@ extern "C" {
struct UTUN_INSTANCE;
struct DB_SYNC_INSTANCE;
// etcp_router service ID
#define ETCP_RT_ID_DB_SYNC 0x20
// etcp service ID: ETCP_RT_ID_DB_SYNC (etcp_bind, см. etcp_api.h)
// Message types
#define DB_MSG_INIT_SYNC 0x01
@@ -91,29 +58,37 @@ struct DB_SYNC_INSTANCE;
// Ed25519 signature size
#define DB_SIG_SIZE 64
// Global lifecycle
// Создать контекст и учесть db_sync_enabled из конфига. 0/-1.
int db_sync_init(struct UTUN_INSTANCE* inst);
// Включить ранее созданный модуль независимо от флага конфига. Повтор допустим; 0/-1.
int db_sync_enable(struct UTUN_INSTANCE* inst);
// Отменить таймеры/обмен, снять обработчики; общую SQLite не закрывает.
void db_sync_destroy(struct UTUN_INSTANCE* inst);
// Instance management
// Зарегистрировать/найти таблицу для group_id. auto_sync запускает начальный обмен; NULL при ошибке.
struct DB_SYNC_INSTANCE* db_sync_instance_add(struct UTUN_INSTANCE* inst, const char* table_name, uint64_t group_id, int auto_sync);
// Освободить экземпляр синхронизации; сама таблица остаётся в БД.
void db_sync_instance_remove(struct DB_SYNC_INSTANCE* si);
// Найти живой инстанс по group_id (поиск в текущем массиве db->instances — указатель всегда актуален).
// Заимствованный указатель по group_id; действителен до remove/destroy, NULL если нет.
struct DB_SYNC_INSTANCE* db_sync_instance_find(struct UTUN_INSTANCE* inst, uint64_t group_id);
// group_id инстанса (для chat-таблиц равен числовому channel_id). Возвращает 0 при si==NULL.
uint64_t db_sync_instance_group_id(struct DB_SYNC_INSTANCE* si);
// Data operations (per-instance)
// db_sync_insert_signed: sig MUST be non-NULL, 64 bytes.
// sig = Ed25519(ts[8] || author[8] || json).
// sig = Ed25519(ts[8] || json); ts записан в порядке байтов хоста.
// ts — call db_sync_next_timestamp(si) before signing to reserve monotonically increasing timestamp.
int db_sync_insert_signed(struct DB_SYNC_INSTANCE* si, const char* json_data, size_t len,
const uint8_t* sig, size_t sig_len, uint64_t ts,
const char* local_attrs);
uint32_t db_sync_count(struct DB_SYNC_INSTANCE* si);
// Последний известный/зарезервированный timestamp в миллисекундах Unix; 0 для NULL.
uint64_t db_sync_get_last_timestamp(struct DB_SYNC_INSTANCE* si);
// Зарезервировать следующий timestamp перед подписью; растёт даже в пределах одной ms.
uint64_t db_sync_next_timestamp(struct DB_SYNC_INSTANCE* si);
// Select: iterate records ordered by (timestamp, author), starting at offset, max limit (0=unlimited).
// Returns number of records passed to callback.
// Обход по (timestamp, author_signature), начиная с offset; limit=0 — без ограничения.
// Возвращает число записей или -1. Данные callback заимствованы на время вызова.
typedef void (*db_sync_select_cb)(void* arg, uint64_t id, uint64_t timestamp,
const char* data, size_t data_len, uint64_t author,
const uint8_t* author_sig, size_t sig_len,
@@ -122,11 +97,13 @@ int db_sync_select(struct DB_SYNC_INSTANCE* si, uint32_t offset, uint32_t l
db_sync_select_cb cb, void* arg);
// Insert callback: fired after local or peer insert succeeds.
// author_node_id = self for local inserts, peer node_id for remote.
// author_node_id — исходный автор записи, не обязательно переславший её пир.
// initial_sync = 1 если запись пришла в рамках bulk-обмена SEND_DATA (initial/re-sync),
// 0 для live-PUSH и локальных вставок.
typedef void (*db_sync_insert_cb)(struct DB_SYNC_INSTANCE* si,
uint64_t record_timestamp,
const char* json_data, size_t len,
uint64_t author_node_id, void* arg);
uint64_t author_node_id, int initial_sync, void* arg);
void db_sync_set_insert_cb(struct DB_SYNC_INSTANCE* si, db_sync_insert_cb cb, void* arg);
// Verify chain integrity: returns 0 if all chain_hashes are correct, 1 if any mismatch found
@@ -135,6 +112,9 @@ int db_sync_chain_verify(struct DB_SYNC_INSTANCE* si);
// Peer state control (for testing — disable PUSH to isolate sync protocol)
void db_sync_peer_set_state(struct DB_SYNC_INSTANCE* si, uint64_t node_id, uint8_t state);
// Per-(chat,peer) «play sound on message» flag: sound_off + sleeping peer → throttled.
void db_sync_set_peer_sound(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t peer_node_id, int sound_on);
// Gate auto-sync: while gated the instance does not auto-initiate sync (waits for
// member_sync/pubkeys). Un-gating launches sync to peers in sync_state==0.
void db_sync_instance_set_gated(struct DB_SYNC_INSTANCE* si, int gated);
+63 -30
View File
@@ -263,8 +263,8 @@ static int collect_node_addrs_from_db(struct UTUN_INSTANCE* inst, uint64_t node_
/* ── публичный API ── */
int chat_invite_build_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id,
const char* password, char* out, size_t out_size) {
static int prepare_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id,
const char* password, char* out, size_t out_size, uint64_t* target, uint64_t* key) {
struct chat_core_ctx* cc = CC(inst);
if (!out || out_size == 0) return -1;
out[0] = '\0';
@@ -347,7 +347,8 @@ int chat_invite_build_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint
}
/* ── регистрация join-ключа на connection-узле (или локально, если target==self) ── */
chat_join_register_key(inst, channel_id, target_nid, data.join_key);
*target = target_nid;
*key = data.join_key;
return 0;
}
@@ -363,6 +364,39 @@ int chat_invite_best_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint6
return 0;
}
struct chat_invite_build {
struct chat_join_registration* registration;
chat_invite_ready_fn callback;
void* arg;
char link[1024];
};
static void invite_registered(void* arg, int result) {
struct chat_invite_build* req = arg;
req->registration = NULL;
req->callback(req->arg, result, result == CHAT_JOIN_OK ? req->link : NULL);
u_free(req);
}
struct chat_invite_build* chat_invite_build_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id,
const char* password, chat_invite_ready_fn callback, void* arg) {
if (!callback) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: missing callback"); return NULL; }
struct chat_invite_build* req = u_calloc(1, sizeof(*req));
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: allocation failed"); return NULL; }
uint64_t target, key;
if (prepare_link(inst, channel_id, target_node_id, password, req->link, sizeof(req->link), &target, &key) != 0) {
u_free(req); return NULL;
}
req->callback = callback; req->arg = arg;
req->registration = chat_join_register_key(inst, channel_id, target, key, invite_registered, req);
if (!req->registration) { u_free(req); return NULL; }
return req;
}
void chat_invite_build_cancel(struct chat_invite_build* req) {
if (req && req->registration) chat_join_cancel_registration(req->registration);
}
int chat_invite_candidate_nodes(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t* out_ids, int max_ids) {
struct chat_core_ctx* cc = CC(inst);
if (!out_ids || max_ids <= 0) return -1;
@@ -405,17 +439,18 @@ int chat_invite_candidate_nodes(struct UTUN_INSTANCE* inst, uint64_t channel_id,
void chat_invite_candidates_trampoline(void* arg) {
struct chat_invite_cand_req* req = (struct chat_invite_cand_req*)arg;
if (!req) return;
struct UTUN_INSTANCE* inst = req->inst;
char ch_id[64];
strncpy(ch_id, req->ch_id, sizeof(ch_id) - 1);
ch_id[sizeof(ch_id) - 1] = '\0';
u_free(arg);
if (!CC(req->inst) || !CC(req->inst)->initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — not initialized"); return; }
if (!CC(inst) || !CC(inst)->initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — not initialized"); return; }
uint64_t channel_id = strtoull(ch_id, NULL, 10);
uint64_t auto_id = 0;
chat_invite_best_node(req->inst, channel_id, &auto_id);
chat_invite_best_node(inst, channel_id, &auto_id);
uint64_t ids[INVITE_ADDR_MAX];
int cnt = chat_invite_candidate_nodes(req->inst, channel_id, ids, INVITE_ADDR_MAX);
int cnt = chat_invite_candidate_nodes(inst, channel_id, ids, INVITE_ADDR_MAX);
if (cnt < 0) cnt = 0;
size_t ch_len = strlen(ch_id);
@@ -428,39 +463,37 @@ void chat_invite_candidates_trampoline(void* arg) {
uint16_t u16 = (uint16_t)cnt;
memcpy(evt + 1 + ch_len + 8, &u16, 2);
for (int i = 0; i < cnt; i++) memcpy(evt + 1 + ch_len + 8 + 2 + i * 8, &ids[i], 8);
chat_event_post(req->inst, CHAT_EVT_INVITE_CANDIDATES, evt, (int)evt_sz);
chat_event_post(inst, CHAT_EVT_INVITE_CANDIDATES, evt, (int)evt_sz);
u_free(evt);
}
void chat_invite_build_link_trampoline(void* arg) {
struct chat_invite_build_req* req = (struct chat_invite_build_req*)arg;
if (!req) return;
uint64_t channel_id = strtoull(req->ch_id, NULL, 10);
uint64_t target = req->target_node_id;
char password[INVITE_PASS_MAX];
password[0] = '\0';
const char* pass = NULL;
if (req->password[0]) {
strncpy(password, req->password, sizeof(password) - 1);
password[sizeof(password) - 1] = '\0';
pass = password;
}
char link[1024];
int ok = chat_invite_build_link(req->inst, channel_id, target, pass, link, sizeof(link));
static void gui_invite_ready(void* arg, int result, const char* link) {
struct chat_invite_build_req* req = arg;
req->inst->invite_gui_request = NULL;
if (result == CHAT_JOIN_CANCELLED) { u_free(req); return; }
size_t ch_len = strlen(req->ch_id);
size_t link_len = (ok == 0) ? strlen(link) : 0;
size_t evt_sz = 1 + ch_len + 2 + link_len;
size_t link_len = link ? strlen(link) : 0;
size_t evt_sz = 1 + ch_len + 8 + 2 + link_len;
uint8_t* evt = u_malloc(evt_sz);
if (!evt) { u_free(req); return; }
if (!evt) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: event allocation failed"); u_free(req); return; }
evt[0] = (uint8_t)ch_len;
memcpy(evt + 1, req->ch_id, ch_len);
uint16_t u16 = (uint16_t)link_len;
memcpy(evt + 1 + ch_len, &u16, 2);
if (link_len) memcpy(evt + 1 + ch_len + 2, link, link_len);
memcpy(evt + 1 + ch_len, &req->request_id, 8);
memcpy(evt + 1 + ch_len + 8, &u16, 2);
if (link_len) memcpy(evt + 1 + ch_len + 10, link, link_len);
chat_event_post(req->inst, CHAT_EVT_INVITE_LINK_READY, evt, (int)evt_sz);
u_free(evt);
u_free(req);
}
void chat_invite_build_link_trampoline(void* arg) {
struct chat_invite_build_req* req = arg;
if (!req) return;
if (!req->inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: GUI request without instance"); u_free(req); return; }
if (req->inst->invite_gui_request) chat_invite_build_cancel(req->inst->invite_gui_request->pending);
req->inst->invite_gui_request = req;
req->pending = chat_invite_build_link(req->inst, strtoull(req->ch_id, NULL, 10), req->target_node_id,
req->password[0] ? req->password : NULL, gui_invite_ready, req);
if (!req->pending) gui_invite_ready(req, CHAT_JOIN_ERROR, NULL);
}
+11 -3
View File
@@ -16,14 +16,20 @@ extern "C" {
#endif
struct UTUN_INSTANCE;
struct chat_invite_build;
typedef void (*chat_invite_ready_fn)(void* arg, int result, const char* link);
/* Собрать invite-ссылку для канала.
* target_node_id = 0 → auto (лучший достижимый мембер, иначе self).
* target_node_id != 0 → конкретный узел (должен быть достижимым мембером канала).
* password = NULL → без пароля.
* Возвращает 0 и заполняет out, либо -1 (детали в логе). */
int chat_invite_build_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id,
const char* password, char* out, size_t out_size);
* NULL=ошибка запуска, callback не вызывается. Иначе callback после возврата,
* ровно один раз, result из enum chat_join_result. link доступен только внутри callback,
* только при успехе — после подтверждения сохранения ключа на выбранном узле.
* Handle действителен до callback; cancel/destroy завершают запрос синхронно с CANCELLED. */
struct chat_invite_build* chat_invite_build_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id,
const char* password, chat_invite_ready_fn callback, void* arg);
void chat_invite_build_cancel(struct chat_invite_build* request);
/* Список достижимых узлов-кандидатов канала (включая self) — тот же фильтр, что в auto.
* Возвращает количество (0..max_ids), либо -1. */
@@ -37,8 +43,10 @@ int chat_invite_best_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint6
struct chat_invite_build_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
uint64_t request_id; /* идентификатор запроса UI, возвращается в событии */
uint64_t target_node_id; /* 0 = auto */
char password[128];
struct chat_invite_build* pending; /* внутреннее поле trampoline */
};
void chat_invite_build_link_trampoline(void* arg);
+8 -1
View File
@@ -6,6 +6,7 @@
*/
#include "invite_link.h"
#include "../transport_layer/secure_channel.h"
#include "../../lib/debug_config.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
@@ -164,7 +165,13 @@ int invite_serialize_addrs(const struct InviteData* data, uint8_t* buf, size_t b
}
int invite_link_encode(const struct InviteData* data, const char* password, char* out, size_t out_size) {
if (!data || !out || data->pubkey[0] == 0) return -1;
if (!data || !out) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "invite_link_encode: missing input or output"); return -1;
}
static const uint8_t empty_key[INVITE_PUBKEY_SIZE] = {0};
if (!memcmp(data->pubkey, empty_key, sizeof(empty_key))) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "invite_link_encode: empty public key"); return -1;
}
if (data->addrCount == 0) return -1;
uint8_t raw[4096]; size_t pos = 0;
raw[pos++] = INVITE_LINK_VERSION;
+359 -267
View File
@@ -1,3 +1,4 @@
#include "../routing_layer/topo_group_connect.h"
#include "member_sync.h"
#include "../routing_layer/topo_node_sqlite.h"
@@ -34,35 +35,66 @@ static void _peers_table(const char* ch_id, char* buf, size_t sz) {
snprintf(buf, sz, "%s", tbl);
}
/* ── Member hash (identical to old _compute_member_hash) ── */
/* Канонические целые для Merkle и wire; формат подписываемых сообщений задаётся отдельно. */
static void member_write64(uint8_t* p, uint64_t n) {
for (int i = 7; i >= 0; i--) { p[i] = (uint8_t)n; n >>= 8; }
}
static void _compute_member_hash(uint64_t node_id, const uint8_t* x25519,
const uint8_t* ed25519,
const uint8_t* join_sig, uint64_t join_ts,
const uint8_t* update_sig, uint64_t update_ts,
const char* adm_tags, const uint8_t* adm_tags_sig,
uint64_t signed_by, const uint8_t* signature,
uint8_t hash_out[MT_HASH_SIZE]) {
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
EVP_DigestInit_ex(ctx, EVP_sha256(), NULL);
EVP_DigestUpdate(ctx, &node_id, 8);
EVP_DigestUpdate(ctx, x25519, 32);
EVP_DigestUpdate(ctx, ed25519, 32);
EVP_DigestUpdate(ctx, join_sig, 64);
EVP_DigestUpdate(ctx, &join_ts, 8);
if (update_sig) EVP_DigestUpdate(ctx, update_sig, 64); else { static const uint8_t z[64]; EVP_DigestUpdate(ctx, z, 64); }
EVP_DigestUpdate(ctx, &update_ts, 8);
static uint64_t member_read64(const uint8_t* p) {
uint64_t n = 0;
for (int i = 0; i < 8; i++) n = (n << 8) | p[i];
return n;
}
uint8_t atl = adm_tags ? (uint8_t)strnlen(adm_tags, 255) : 0;
EVP_DigestUpdate(ctx, &atl, 1);
if (atl) EVP_DigestUpdate(ctx, adm_tags, atl);
if (adm_tags_sig) EVP_DigestUpdate(ctx, adm_tags_sig, 64); else { static const uint8_t z64[64]; EVP_DigestUpdate(ctx, z64, 64); }
int member_sync_build_owner_msg(uint64_t node_id, const char* tags, uint8_t* out, size_t capacity) {
size_t len = tags ? strlen(tags) : 0;
if (!tags || !out || len > MS_ADM_TAGS_MAX || capacity < len + 8) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid owner message node=%016llx bytes=%zu capacity=%zu max=%d",
MS_ID, (unsigned long long)node_id, len, capacity, MS_ADM_TAGS_MAX);
return -1;
}
memcpy(out, tags, len);
memcpy(out + len, &node_id, 8);
return (int)len + 8;
}
EVP_DigestUpdate(ctx, &signed_by, 8);
if (signature) EVP_DigestUpdate(ctx, signature, 64); else { static const uint8_t zsig[64]; EVP_DigestUpdate(ctx, zsig, 64); }
static int _compute_member_hash(uint64_t node_id, const uint8_t* x25519, const uint8_t* ed25519,
const uint8_t* join_sig, uint64_t join_ts, const uint8_t* update_sig, uint64_t update_ts,
const char* userinfo, const char* adm_tags, const uint8_t* adm_tags_sig,
uint64_t signed_by, const uint8_t* signature, uint8_t hash_out[MT_HASH_SIZE]) {
uint8_t data[MS_ADM_TAGS_MAX + 1024];
size_t off = 0;
member_write64(data + off, node_id); off += 8;
memcpy(data + off, x25519, 32); off += 32;
memcpy(data + off, ed25519, 32); off += 32;
if (join_sig) memcpy(data + off, join_sig, 64); else memset(data + off, 0, 64);
off += 64; member_write64(data + off, join_ts); off += 8;
if (update_sig) memcpy(data + off, update_sig, 64); else memset(data + off, 0, 64);
off += 64; member_write64(data + off, update_ts); off += 8;
size_t ul = userinfo ? strlen(userinfo) : 0, atl = adm_tags ? strlen(adm_tags) : 0;
if (ul > 255 || atl > MS_ADM_TAGS_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member strings exceed wire limits node=%016llx", MS_ID, (unsigned long long)node_id);
return -1;
}
data[off++] = (uint8_t)ul;
if (ul) { memcpy(data + off, userinfo, ul); off += ul; }
data[off++] = (uint8_t)(atl >> 8); data[off++] = (uint8_t)atl;
if (atl) { memcpy(data + off, adm_tags, atl); off += atl; }
if (adm_tags_sig) memcpy(data + off, adm_tags_sig, 64); else memset(data + off, 0, 64);
off += 64; member_write64(data + off, signed_by); off += 8;
if (signature) memcpy(data + off, signature, 64); else memset(data + off, 0, 64);
off += 64;
if (EVP_Digest(data, off, hash_out, NULL, EVP_sha256(), NULL) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member hash failed node=%016llx", MS_ID, (unsigned long long)node_id);
return -1;
}
return 0;
}
EVP_DigestFinal_ex(ctx, hash_out, NULL);
EVP_MD_CTX_free(ctx);
/* SQLite BLOB pointers are only safe after checking their exact length. */
static int member_blob(sqlite3_stmt* st, int col, int size, int optional) {
int bytes = sqlite3_column_bytes(st, col);
return bytes == size || (optional && bytes == 0);
}
int member_sync_build_update_msg(const uint8_t* join_sig, uint64_t update_ts,
@@ -114,7 +146,9 @@ static int _member_update_bucket_hash(void* ctx, const char* ns, uint8_t level,
uint64_t prefix64, EVP_MD_CTX* sha_ctx) {
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)ctx;
sqlite3* db = _db(inst); if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bucket_hash — db is NULL", MS_ID); return -1; }
if (level == 0) return 0;
if (level != MT_MAX_LEVEL) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: expected leaf, level=%u", MS_ID, level); return -1;
}
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bucket_hash ns=%s L%d/P%016llx", MS_ID, ns, level, (unsigned long long)prefix64);
char peers_tbl[128]; _peers_table(ns, peers_tbl, sizeof(peers_tbl));
@@ -123,7 +157,7 @@ static int _member_update_bucket_hash(void* ctx, const char* ns, uint8_t level,
char sql[512]; snprintf(sql, sizeof(sql),
"SELECT node_id, x25519_pubkey, ed25519_pubkey,"
" join_sig, join_ts, update_sig, update_ts, adm_tags, adm_tags_sig, signed_by, signature, source"
" join_sig, join_ts, update_sig, update_ts, adm_tags, adm_tags_sig, signed_by, signature, source, userinfo"
" FROM \"%s\""
" WHERE (node_id & %lld) == %lld ORDER BY node_id ASC",
peers_tbl, (long long)mask, (long long)prefix64);
@@ -132,7 +166,8 @@ static int _member_update_bucket_hash(void* ctx, const char* ns, uint8_t level,
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bucket_hash prepare failed ns=%s", MS_ID, ns); return -1; }
int count = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
int step;
while ((step = sqlite3_step(stmt)) == SQLITE_ROW) {
uint64_t nid = (uint64_t)sqlite3_column_int64(stmt, 0);
const uint8_t* x25 = (const uint8_t*)sqlite3_column_blob(stmt, 1);
const uint8_t* ed = (const uint8_t*)sqlite3_column_blob(stmt, 2);
@@ -145,23 +180,35 @@ static int _member_update_bucket_hash(void* ctx, const char* ns, uint8_t level,
uint64_t sb = (uint64_t)sqlite3_column_int64(stmt, 9);
const uint8_t* sgn = (const uint8_t*)sqlite3_column_blob(stmt, 10);
int src = sqlite3_column_int(stmt, 11);
if (!x25 || !ed || src != 0) continue;
if (src != 0) continue;
if (!member_blob(stmt, 1, 32, 0) || !member_blob(stmt, 2, 32, 0) || !member_blob(stmt, 3, 64, 1)
|| !member_blob(stmt, 5, 64, 1) || !member_blob(stmt, 8, 64, 1) || !member_blob(stmt, 10, 64, 1)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid hash record ns=%s node=%016llx", MS_ID, ns, (unsigned long long)nid);
sqlite3_finalize(stmt); return -1;
}
uint8_t mh[MT_HASH_SIZE];
_compute_member_hash(nid, x25, ed, sig, jts, usig, uts, atags, atsig, sb, sgn, mh);
EVP_DigestUpdate(sha_ctx, mh, MT_HASH_SIZE);
const char* userinfo = (const char*)sqlite3_column_text(stmt, 12);
if (_compute_member_hash(nid, x25, ed, sig, jts, usig, uts, userinfo, atags, atsig, sb, sgn, mh) < 0
|| EVP_DigestUpdate(sha_ctx, mh, MT_HASH_SIZE) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: leaf digest failed ns=%s", MS_ID, ns);
sqlite3_finalize(stmt); return -1;
}
count++;
}
sqlite3_finalize(stmt);
if (step != SQLITE_DONE) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: leaf read failed ns=%s: %s", MS_ID, ns, sqlite3_errmsg(db)); return -1;
}
return count;
}
static int _member_get_items(void* ctx, const char* ns, uint8_t level,
uint64_t prefix, uint8_t prefix_bytes,
uint8_t* buf, size_t* len) {
static int _member_get_page(void* ctx, const char* ns, uint64_t prefix, int after_valid, uint64_t after,
uint8_t* buf, size_t* len, uint64_t* next, int* more) {
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)ctx;
sqlite3* db = _db(inst); if (!db || !buf || !len) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: get_items — invalid args db=%p buf=%p len=%p", MS_ID, (void*)db, (void*)buf, (void*)len); return -1; }
if (level == 0) { *len = 0; return 0; }
const uint8_t level = MT_MAX_LEVEL;
*next = 0; *more = 0;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: get_items ns=%s L%d/P%016llx", MS_ID, ns, level, (unsigned long long)prefix);
char peers_tbl[128]; _peers_table(ns, peers_tbl, sizeof(peers_tbl));
@@ -173,17 +220,20 @@ static int _member_get_items(void* ctx, const char* ns, uint8_t level,
" join_sig, join_ts, update_sig, update_ts, userinfo, adm_tags, adm_tags_sig,"
" signed_by, signature, source"
" FROM \"%s\""
" WHERE (node_id & %lld) == %lld ORDER BY node_id ASC",
" WHERE (node_id & %lld) == %lld AND (?=0 OR node_id>?) ORDER BY node_id ASC",
peers_tbl, (long long)mask, (long long)prefix);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: get_items prepare failed ns=%s", MS_ID, ns); return -1; }
sqlite3_bind_int(stmt, 1, after_valid);
sqlite3_bind_int64(stmt, 2, (sqlite3_int64)after);
size_t off = 0;
if (off + 2 > *len) { sqlite3_finalize(stmt); return -2; }
uint16_t* cnt = (uint16_t*)(buf + off); off += 2; *cnt = 0;
uint16_t cnt = 0; off += 2;
while (sqlite3_step(stmt) == SQLITE_ROW) {
int step;
while ((step = sqlite3_step(stmt)) == SQLITE_ROW) {
uint64_t nid = (uint64_t)sqlite3_column_int64(stmt, 0);
const uint8_t* x25 = (const uint8_t*)sqlite3_column_blob(stmt, 1);
const uint8_t* ed = (const uint8_t*)sqlite3_column_blob(stmt, 2);
@@ -197,43 +247,56 @@ static int _member_get_items(void* ctx, const char* ns, uint8_t level,
uint64_t sb = (uint64_t)sqlite3_column_int64(stmt, 10);
const uint8_t* sgn = (const uint8_t*)sqlite3_column_blob(stmt, 11);
int src = sqlite3_column_int(stmt, 12);
if (!x25 || !ed || src != 0) continue;
if (src != 0) continue;
if (!member_blob(stmt, 1, 32, 0) || !member_blob(stmt, 2, 32, 0) || !member_blob(stmt, 3, 64, 1)
|| !member_blob(stmt, 5, 64, 1) || !member_blob(stmt, 9, 64, 1) || !member_blob(stmt, 11, 64, 1)
|| (nm && strlen(nm) > 255) || (atags && strlen(atags) > MS_ADM_TAGS_MAX)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid page record ns=%s node=%016llx", MS_ID, ns, (unsigned long long)nid);
sqlite3_finalize(stmt); return -1;
}
uint8_t nl = nm ? (uint8_t)strnlen(nm, 255) : 0;
uint8_t atl = atags ? (uint8_t)strnlen(atags, 255) : 0;
size_t atl = atags ? strlen(atags) : 0;
uint8_t flags = (sig && jts) ? PEERS_FLAG_HAS_JOIN : 0;
size_t need = 8 + 32 + 32 + 1 + (flags ? 72ULL : 0ULL) + 64 + 8 + 1 + (size_t)nl + 1 + (size_t)atl + 64 + 8 + 64;
if (off + need > *len) { sqlite3_finalize(stmt); return -2; }
memcpy(buf + off, &nid, 8); off += 8;
size_t need = 8 + 32 + 32 + 1 + (flags ? 72ULL : 0ULL) + 64 + 8 + 1 + (size_t)nl + 2 + atl + 64 + 8 + 64;
if (off + need > *len) {
if (!cnt) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record exceeds page capacity ns=%s", MS_ID, ns); sqlite3_finalize(stmt); return -1; }
*more = 1;
break;
}
member_write64(buf + off, nid); off += 8;
memcpy(buf + off, x25, 32); off += 32;
memcpy(buf + off, ed, 32); off += 32;
buf[off++] = flags;
if (flags & PEERS_FLAG_HAS_JOIN) {
memcpy(buf + off, sig, 64); off += 64;
memcpy(buf + off, &jts, 8); off += 8;
member_write64(buf + off, jts); off += 8;
}
if (usig && uts) {
memcpy(buf + off, usig, 64); off += 64;
memcpy(buf + off, &uts, 8); off += 8;
member_write64(buf + off, uts); off += 8;
} else {
memset(buf + off, 0, 64); off += 64;
uint64_t z = 0; memcpy(buf + off, &z, 8); off += 8;
}
buf[off++] = nl;
if (nl) { memcpy(buf + off, nm, nl); off += nl; }
buf[off++] = atl;
buf[off++] = (uint8_t)(atl >> 8); buf[off++] = (uint8_t)atl;
if (atl) { memcpy(buf + off, atags, atl); off += atl; }
if (atsig && sqlite3_column_bytes(stmt, 9) >= 64) { memcpy(buf + off, atsig, 64); off += 64; }
else { memset(buf + off, 0, 64); off += 64; }
memcpy(buf + off, &sb, 8); off += 8;
member_write64(buf + off, sb); off += 8;
if (sgn && sqlite3_column_bytes(stmt, 11) >= 64) { memcpy(buf + off, sgn, 64); off += 64; }
else { memset(buf + off, 0, 64); off += 64; }
(*cnt)++;
cnt++;
*next = nid;
}
if (step != SQLITE_DONE && !*more) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page read failed ns=%s: %s", MS_ID, ns, sqlite3_errmsg(db)); sqlite3_finalize(stmt); return -1; }
sqlite3_finalize(stmt);
buf[0] = (uint8_t)(cnt >> 8); buf[1] = (uint8_t)cnt;
*len = off;
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: get_items ns=%s L%d/P%016llx mask=%016llx count=%u off=%zu",
MS_ID, ns, level, (unsigned long long)prefix, (unsigned long long)mask, *cnt, off);
MS_ID, ns, level, (unsigned long long)prefix, (unsigned long long)mask, cnt, off);
return 0;
}
@@ -314,7 +377,7 @@ static void _ms_on_bgp_node(struct TOPO_GROUP* group, uint64_t node_id, int even
/* REMOVE не удаляет member (удаление — только битая подпись при верификации) */
char tags[256] = "";
char tags[MS_ADM_TAGS_MAX + 1] = "";
char peers_tbl[128]; _peers_table(group->channel_id, peers_tbl, sizeof(peers_tbl));
sqlite3_stmt* st = NULL;
char sql[256]; snprintf(sql, sizeof(sql), "SELECT COALESCE(adm_tags,'') FROM \"%s\" WHERE node_id=?", peers_tbl);
@@ -327,8 +390,8 @@ static void _ms_on_bgp_node(struct TOPO_GROUP* group, uint64_t node_id, int even
sqlite3_finalize(st);
}
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bgp_node ev=%d nid=0x%016llx tags=%s",
MS_ID, event, (unsigned long long)node_id, tags);
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bgp_node ev=%d nid=0x%016llx tags_bytes=%zu",
MS_ID, event, (unsigned long long)node_id, strlen(tags));
_fire_props_changed(inst, node_id, tags[0] ? tags : NULL, group->channel_id);
}
@@ -416,7 +479,7 @@ int member_sync_validate_pubkey(struct UTUN_INSTANCE* inst, const char* ch_id,
return topo_node_sqlite_member_in_channel(db, ch_id, node_id);
}
int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
static int member_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
uint64_t from_peer, const struct ms_member_rec* m) {
if (!inst || !ch_id || !m || !m->x25519 || !m->ed25519) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record — invalid args", MS_ID);
@@ -425,9 +488,9 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
sqlite3* db = _db(inst); if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record — db is NULL", MS_ID); return -1; }
int stale = 0, changed = 0;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record ch=%s nid=%016llx uts=%llu tags=%s",
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record ch=%s nid=%016llx uts=%llu tags_bytes=%zu",
MS_ID, ch_id, (unsigned long long)m->node_id, (unsigned long long)m->update_ts,
m->adm_tags ? m->adm_tags : "");
m->adm_tags ? strlen(m->adm_tags) : 0);
char peers_tbl[128]; _peers_table(ch_id, peers_tbl, sizeof(peers_tbl));
@@ -435,16 +498,20 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
int has_local = 0;
uint8_t local_join_sig[64] = {0}; uint64_t local_join_ts = 0;
uint64_t local_update_ts = 0;
char local_tags[256] = "";
char local_tags[MS_ADM_TAGS_MAX + 1] = "";
uint64_t local_signed_by = 0;
uint8_t local_signature[64] = {0};
int local_src = 0;
{
sqlite3_stmt* st = NULL;
char sql[512]; snprintf(sql, sizeof(sql),
"SELECT join_sig, join_ts, update_ts, adm_tags, signed_by, signature FROM \"%s\" WHERE node_id=?", peers_tbl);
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)m->node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
"SELECT join_sig, join_ts, update_ts, adm_tags, signed_by, signature, source FROM \"%s\" WHERE node_id=?", peers_tbl);
int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)m->node_id);
if (rc == SQLITE_OK) {
rc = sqlite3_step(st);
if (rc == SQLITE_ROW && (!member_blob(st, 0, 64, 1) || !member_blob(st, 5, 64, 1))) rc = SQLITE_CORRUPT;
if (rc == SQLITE_ROW) {
has_local = 1;
const uint8_t* js = (const uint8_t*)sqlite3_column_blob(st, 0);
if (js) memcpy(local_join_sig, js, 64);
@@ -455,8 +522,13 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
local_signed_by = (uint64_t)sqlite3_column_int64(st, 4);
const uint8_t* ls = (const uint8_t*)sqlite3_column_blob(st, 5);
if (ls && sqlite3_column_bytes(st, 5) >= 64) memcpy(local_signature, ls, 64);
local_src = sqlite3_column_int(st, 6);
}
sqlite3_finalize(st);
}
sqlite3_finalize(st);
if (rc != SQLITE_ROW && rc != SQLITE_DONE) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: local record read failed ns=%s node=%016llx rc=%d: %s",
MS_ID, ch_id, (unsigned long long)m->node_id, rc, sqlite3_errmsg(db)); return MT_ERR_IO;
}
}
@@ -542,18 +614,16 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
adm_storage = (json_flat_get(m->adm_tags, "storage", ver_str, sizeof(ver_str)) == 0 && strcmp(ver_str, "yes") == 0) ? 1 : 0;
uint8_t ch_ed_pub[32] = {0};
if (topo_node_sqlite_channel_get(db, ch_id, NULL, 0, NULL, NULL, ch_ed_pub, NULL) == 0) {
uint8_t amsg[264]; size_t aoff = 0;
size_t atl2 = strlen(m->adm_tags);
if (atl2 > 191) atl2 = 191;
memcpy(amsg + aoff, m->adm_tags, atl2); aoff += atl2;
memcpy(amsg + aoff, &m->node_id, 8); aoff += 8;
uint8_t amsg[MS_ADM_TAGS_MAX + 8];
int aoff = member_sync_build_owner_msg(m->node_id, m->adm_tags, amsg, sizeof(amsg));
if (aoff < 0) return MT_ERR_DATA;
EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, ch_ed_pub, 32);
if (pkey) {
EVP_MD_CTX* vctx = EVP_MD_CTX_new();
if (vctx) {
int ok = (EVP_DigestVerifyInit(vctx, NULL, NULL, NULL, pkey) == 1)
&& (EVP_DigestVerify(vctx, m->adm_tags_sig, 64, amsg, aoff) == 1);
if (!ok) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record invalid adm_tags_sig node=0x%016llx ns=%s tags=%s", MS_ID, (unsigned long long)m->node_id, ch_id, m->adm_tags);
if (!ok) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record invalid adm_tags_sig node=0x%016llx ns=%s bytes=%zu", MS_ID, (unsigned long long)m->node_id, ch_id, strlen(m->adm_tags));
else block_b_ok = 1;
EVP_MD_CTX_free(vctx);
}
@@ -572,6 +642,15 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
if (adm_ver > local_ver) block_b_changed = 1;
else if (adm_ver < local_ver) block_b_stale = 1;
}
if (block_b_changed && local_src != 0)
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: adm_tags change on placeholder member (source=%d) — ignored by merkle, won't propagate nid=0x%016llx ns=%s",
MS_ID, local_src, (unsigned long long)m->node_id, ch_id);
if (!identity_ok || !block_a_ok || (m->adm_tags && m->adm_tags[0] && !block_b_ok)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: rejected record ns=%s node=%016llx identity=%d member=%d owner=%d",
MS_ID, ch_id, (unsigned long long)m->node_id, identity_ok, block_a_ok, block_b_ok);
return MT_ERR_DATA;
}
/* ── запись изменённых блоков ── */
if (block_a_changed) {
@@ -589,9 +668,12 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
/* узел в nodes пишет только merkle-путь (verified identity) */
char node_name[128] = "";
if (m->userinfo) json_flat_get(m->userinfo, "name", node_name, sizeof(node_name));
topo_node_sqlite_node_update_verified(db, m->node_id, node_name, m->x25519, m->ed25519,
if (topo_node_sqlite_node_update_verified(db, m->node_id, node_name, m->x25519, m->ed25519,
m->update_ts ? m->update_ts : m->join_ts,
ntp_time_get_seconds(inst));
ntp_time_get_seconds(inst)) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: node update failed ns=%s node=%016llx", MS_ID, ch_id,
(unsigned long long)m->node_id); return MT_ERR_IO;
}
}
if (block_b_changed) {
if (topo_node_sqlite_member_owner_put(db, ch_id, m->node_id, m->adm_tags, m->adm_tags_sig, adm_storage) != 0) {
@@ -604,8 +686,10 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
if (block_a_stale || block_b_stale) stale = 1;
/* пересчёт классификации (node_type/storage) — DB-хелпер, читает node_addresses (адреса BGP) */
if (changed)
topo_node_sqlite_nodeinfo_updated(db, m->node_id);
if (changed && topo_node_sqlite_nodeinfo_updated(db, m->node_id) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: classification update failed ns=%s node=%016llx", MS_ID, ch_id,
(unsigned long long)m->node_id); return MT_ERR_IO;
}
/* ── recompute и подтвердить реальное изменение по хешу ── */
if (changed) {
@@ -614,21 +698,39 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
if (rc == 0) changed = 0; /* запись была no-op (данные идентичны) */
}
/* ── пассивное добавление: если узел уже подключён напрямую — добавить в CHAT-группу ── */
if (changed && inst->topo_groups) {
uint64_t gid = strtoull(ch_id, NULL, 10);
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid);
if (g && g->group_type == TOPO_GROUP_TYPE_CHAT) {
struct ETCP_CONN* c = instance_find_conn(inst, m->node_id);
if (c) topo_group_new_conn(g, c);
}
}
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record ch=%s nid=0x%016llx → changed=%d stale=%d",
MS_ID, ch_id, (unsigned long long)m->node_id, changed, stale);
return (changed ? MS_APPLY_CHANGED : 0) | (stale ? MS_APPLY_STALE : 0);
}
static void member_committed(struct UTUN_INSTANCE* inst, const char* ns, uint64_t node_id) {
merkle_sync_changed(inst, ns);
struct TOPO_GROUP* g = inst->topo_groups ? topo_groups_find(inst->topo_groups, strtoull(ns, NULL, 10)) : NULL;
struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
if (g && g->group_type == TOPO_GROUP_TYPE_CHAT && conn) topo_group_connect_node_once(g, node_id);
}
int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
uint64_t from_peer, const struct ms_member_rec* m) {
sqlite3* db = _db(inst);
if (!db || !ch_id || !m) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid apply args", MS_ID); return -1; }
int own = sqlite3_get_autocommit(db);
if (sqlite3_exec(db, "SAVEPOINT member_record", NULL, NULL, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record begin: %s", MS_ID, sqlite3_errmsg(db)); return -1;
}
int rc = member_apply_record(inst, ch_id, from_peer, m);
if (rc < 0 && sqlite3_exec(db, "ROLLBACK TO member_record", NULL, NULL, NULL) != SQLITE_OK)
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record rollback: %s", MS_ID, sqlite3_errmsg(db));
if (sqlite3_exec(db, "RELEASE member_record", NULL, NULL, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record commit: %s", MS_ID, sqlite3_errmsg(db));
if (sqlite3_exec(db, "ROLLBACK TO member_record; RELEASE member_record", NULL, NULL, NULL) != SQLITE_OK)
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record cleanup: %s", MS_ID, sqlite3_errmsg(db));
return -1;
}
if (own && rc >= 0 && (rc & MS_APPLY_CHANGED)) member_committed(inst, ch_id, m->node_id);
return rc;
}
/* ── Верификация локальной записи при чтении из БД ──
Единственный сценарий удаления мембера: подпись присутствует и невалидна,
либо node_id != derive(x25519). Отсутствие подписи = «не верифицировано», не удаляем. */
@@ -671,11 +773,9 @@ int member_sync_verify_local_record(struct sqlite3* db, const char* ch_id, const
uint8_t ch_ed_pub[32] = {0};
if (topo_node_sqlite_channel_get(db, ch_id, NULL, 0, NULL, NULL, ch_ed_pub, NULL) != 0)
return 1; /* нет канального ключа — не можем проверить, не удаляем */
uint8_t amsg[264]; size_t aoff = 0;
size_t atl2 = strlen(m->adm_tags);
if (atl2 > 191) atl2 = 191;
memcpy(amsg + aoff, m->adm_tags, atl2); aoff += atl2;
memcpy(amsg + aoff, &m->node_id, 8); aoff += 8;
uint8_t amsg[MS_ADM_TAGS_MAX + 8];
int aoff = member_sync_build_owner_msg(m->node_id, m->adm_tags, amsg, sizeof(amsg));
if (aoff < 0) return 0;
EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, ch_ed_pub, 32);
if (!pkey) return 1;
EVP_MD_CTX* vctx = EVP_MD_CTX_new();
@@ -684,8 +784,8 @@ int member_sync_verify_local_record(struct sqlite3* db, const char* ch_id, const
if (vctx) EVP_MD_CTX_free(vctx);
EVP_PKEY_free(pkey);
if (!ok) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_local — invalid adm_tags_sig nid=0x%016llx ns=%s tags=%s",
MS_ID, (unsigned long long)m->node_id, ch_id, m->adm_tags);
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_local — invalid adm_tags_sig nid=0x%016llx ns=%s bytes=%zu",
MS_ID, (unsigned long long)m->node_id, ch_id, strlen(m->adm_tags));
return 0;
}
}
@@ -756,7 +856,7 @@ int member_sync_verify_and_purge(struct UTUN_INSTANCE* inst, const char* ch_id)
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid);
if (g && g->group_type == TOPO_GROUP_TYPE_CHAT) {
struct ETCP_CONN* c = instance_find_conn(inst, nid);
if (c) topo_group_remove_conn(g, c);
if (c) topo_group_remove_conn(g, c, TOPO_REMOVE_MEMBER_INVALID);
}
}
purged++;
@@ -784,74 +884,102 @@ void member_sync_verify_and_purge_all(struct UTUN_INSTANCE* inst) {
sqlite3_finalize(st);
}
static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer,
const uint8_t* data, size_t len) {
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)ctx;
if (len < 2) { DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_items — no items from peer=%016llx ns=%.*s (empty, sync complete)", MS_ID, (unsigned long long)from_peer, (int)strnlen(ns, 32), ns); return 0; }
struct member_wire {
struct ms_member_rec rec;
char userinfo[256], tags[MS_ADM_TAGS_MAX + 1];
uint8_t local_join[64];
};
uint16_t count; memcpy(&count, data, 2);
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_items ns=%s count=%u from=%016llx", MS_ID, ns, count, (unsigned long long)from_peer);
const uint8_t* mp = data + 2; size_t mrem = len - 2;
/* Одна строгая раскладка для проверки и применения. Нельзя молча принимать усечённую запись. */
static int member_decode(const uint8_t* p, size_t len, struct member_wire* w, size_t* used) {
memset(w, 0, sizeof(*w));
if (len < 73) return -1;
struct ms_member_rec* m = &w->rec;
size_t off = 0;
m->node_id = member_read64(p + off); off += 8;
m->x25519 = p + off; off += 32; m->ed25519 = p + off; off += 32;
uint8_t flags = p[off++];
if (flags & ~PEERS_FLAG_HAS_JOIN) return -1;
if (flags & PEERS_FLAG_HAS_JOIN) {
if (len - off < 72) return -1;
m->join_sig = p + off; off += 64; m->join_ts = member_read64(p + off); off += 8;
}
if (len - off < 73) return -1;
m->update_sig = p + off; off += 64; m->update_ts = member_read64(p + off); off += 8;
uint8_t n = p[off++];
if (len - off < (size_t)n + 2 || memchr(p + off, 0, n)) return -1;
memcpy(w->userinfo, p + off, n); off += n;
size_t tags_len = ((size_t)p[off] << 8) | p[off + 1]; off += 2;
if (tags_len > MS_ADM_TAGS_MAX || len - off < tags_len + 136 || memchr(p + off, 0, tags_len)) return -1;
memcpy(w->tags, p + off, tags_len); off += tags_len;
m->adm_tags_sig = p + off; off += 64;
m->signed_by = member_read64(p + off); off += 8;
m->signature = p + off; off += 64;
m->userinfo = w->userinfo; m->adm_tags = w->tags;
*used = off;
return 0;
}
static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer, const uint8_t* data, size_t len) {
struct UTUN_INSTANCE* inst = ctx;
sqlite3* db = _db(inst);
int changed_count = 0;
for (uint16_t i = 0; i < count && mrem >= 73; i++) {
uint64_t nid; memcpy(&nid, mp, 8); mp += 8; mrem -= 8;
const uint8_t* x25 = mp; mp += 32; mrem -= 32;
const uint8_t* ed = mp; mp += 32; mrem -= 32;
uint8_t flags = *mp++; mrem--;
const uint8_t* jsig = NULL; uint64_t jts = 0;
if (flags & PEERS_FLAG_HAS_JOIN) {
if (mrem < 72) break; jsig = mp; mp += 64; mrem -= 64; memcpy(&jts, mp, 8); mp += 8; mrem -= 8;
} else {
/* lookup join from local DB */
uint8_t buf_jsig[64]; uint64_t buf_jts;
if (db && topo_node_sqlite_member_get_join(db, ns, nid, buf_jsig, &buf_jts) == 0) { jsig = buf_jsig; jts = buf_jts; }
if (!db || len < 2) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid member page ns=%s bytes=%zu", MS_ID, ns, len); return MT_ERR_DATA;
}
uint16_t count = ((uint16_t)data[0] << 8) | data[1];
if (count > (len - 2) / 284) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: impossible page count=%u bytes=%zu", MS_ID, count, len); return MT_ERR_DATA;
}
size_t off = 2;
for (unsigned i = 0; i < count; i++) {
struct member_wire w; size_t used;
if (member_decode(data + off, len - off, &w, &used) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: malformed member page ns=%s record=%u offset=%zu", MS_ID, ns, i, off);
return MT_ERR_DATA;
}
if (mrem < 72) break;
const uint8_t* usig = mp; mp += 64; mrem -= 64; uint64_t uts; memcpy(&uts, mp, 8); mp += 8; mrem -= 8;
uint8_t nl = *mp++; mrem--;
char nm[256] = "";
if (nl && mrem >= nl) { memcpy(nm, mp, nl); nm[nl] = '\0'; mp += nl; mrem -= nl; }
if (mrem < 1) break;
uint8_t atl = *mp++; mrem--;
char atags[256] = "";
const uint8_t* atsig = NULL;
if (atl && mrem >= atl) { memcpy(atags, mp, atl); atags[atl] = '\0'; mp += atl; mrem -= atl; }
if (mrem >= 64) { atsig = mp; mp += 64; mrem -= 64; }
uint64_t sb = 0; const uint8_t* sgn = NULL;
if (mrem >= 72) { memcpy(&sb, mp, 8); mp += 8; mrem -= 8; sgn = mp; mp += 64; mrem -= 64; }
struct ms_member_rec m;
memset(&m, 0, sizeof(m));
m.node_id = nid;
m.x25519 = x25; m.ed25519 = ed;
m.join_sig = jsig; m.join_ts = jts;
m.update_sig = usig; m.update_ts = uts;
m.userinfo = nm;
m.adm_tags = atags[0] ? atags : NULL;
m.adm_tags_sig = atsig;
m.signed_by = sb; m.signature = sgn;
int r = member_sync_apply_record(inst, ns, from_peer, &m);
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member_sync recv ns=%s nid=0x%016llx r=%d", MS_ID, ns, (unsigned long long)nid, r);
if (r < 0) continue;
if (r & MS_APPLY_CHANGED) {
changed_count++;
/* fire node_props_changed callbacks */
_fire_props_changed(inst, nid, atags[0] ? atags : NULL, ns);
/* fire apply callbacks (ready-хендлинг join на connection-узле) */
_fire_apply_cbk(inst, ns, nid, x25, sb, sgn);
}
if (r & MS_APPLY_STALE) {
/* у нас версия новее — отправить наш полный рекорд автору */
member_sync_send_to(inst, ns, nid, from_peer);
off += used;
}
if (off != len) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: trailing page bytes ns=%s bytes=%zu", MS_ID, ns, len - off); return MT_ERR_DATA; }
if (!count) return 0;
uint8_t* changed = u_calloc(count, 1);
if (!changed) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page allocation failed", MS_ID); return MT_ERR_IO; }
if (sqlite3_exec(db, "SAVEPOINT member_page", NULL, NULL, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page begin: %s", MS_ID, sqlite3_errmsg(db)); u_free(changed); return MT_ERR_IO;
}
int rc = 0;
off = 2;
for (unsigned i = 0; i < count; i++) {
struct member_wire w; size_t used;
if (member_decode(data + off, len - off, &w, &used) < 0) { rc = MT_ERR_DATA; break; }
if (!w.rec.join_sig && topo_node_sqlite_member_get_join(db, ns, w.rec.node_id, w.local_join, &w.rec.join_ts) == 0)
w.rec.join_sig = w.local_join;
int r = member_sync_apply_record(inst, ns, from_peer, &w.rec);
if (r < 0) { rc = r; break; }
changed[i] = (r & MS_APPLY_CHANGED) != 0;
off += used;
}
if (rc < 0 && sqlite3_exec(db, "ROLLBACK TO member_page", NULL, NULL, NULL) != SQLITE_OK)
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page rollback: %s", MS_ID, sqlite3_errmsg(db));
if (sqlite3_exec(db, "RELEASE member_page", NULL, NULL, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page commit: %s", MS_ID, sqlite3_errmsg(db)); rc = MT_ERR_IO;
if (sqlite3_exec(db, "ROLLBACK TO member_page; RELEASE member_page", NULL, NULL, NULL) != SQLITE_OK)
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page cleanup: %s", MS_ID, sqlite3_errmsg(db));
}
if (rc == 0) {
off = 2;
for (unsigned i = 0; i < count; i++) {
struct member_wire w; size_t used;
if (member_decode(data + off, len - off, &w, &used) < 0) { rc = MT_ERR_DATA; break; }
if (changed[i]) {
member_committed(inst, ns, w.rec.node_id);
_fire_props_changed(inst, w.rec.node_id, w.tags[0] ? w.tags : NULL, ns);
_fire_apply_cbk(inst, ns, w.rec.node_id, w.rec.x25519, w.rec.signed_by, w.rec.signature);
}
off += used;
}
}
if (changed_count > 0)
merkle_sync_broadcast(inst, ns, from_peer, data, len);
return 0;
u_free(changed);
return rc;
}
static int _member_validate_peer(void* ctx, const char* ns, uint64_t peer) {
@@ -861,45 +989,83 @@ static int _member_validate_peer(void* ctx, const char* ns, uint64_t peer) {
return topo_node_sqlite_member_in_channel(db, ns, peer);
}
static int member_finish(void* ctx, const char* ns) {
sqlite3* db = _db(ctx);
char table[128], sql[320]; _peers_table(ns, table, sizeof(table));
snprintf(sql, sizeof(sql), "SELECT node_id,x25519_pubkey,signed_by,signature FROM \"%s\" WHERE source=0", table);
sqlite3_stmt* st = NULL;
int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL);
if (rc != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: finish query ns=%s: %s", MS_ID, ns, sqlite3_errmsg(db)); return -1;
}
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
struct ms_member_rec m = {0};
m.node_id = (uint64_t)sqlite3_column_int64(st, 0);
m.x25519 = sqlite3_column_blob(st, 1);
m.signed_by = (uint64_t)sqlite3_column_int64(st, 2);
m.signature = sqlite3_column_blob(st, 3);
if (!m.signature || sqlite3_column_bytes(st, 3) == 0) continue;
if (sqlite3_column_bytes(st, 1) != 32 || sqlite3_column_bytes(st, 3) != 64) { rc = SQLITE_CORRUPT; break; }
if (_sig_is_zero64(m.signature)) continue;
int verified = member_sync_verify_signature(db, ns, &m);
if (verified != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cannot confirm ns=%s node=%016llx signer=%016llx verification=%d",
MS_ID, ns, (unsigned long long)m.node_id, (unsigned long long)m.signed_by, verified);
rc = SQLITE_CONSTRAINT; break;
}
}
sqlite3_finalize(st);
if (rc != SQLITE_DONE) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: finish failed ns=%s rc=%d", MS_ID, ns, rc); return -1; }
return 0;
}
static const struct merkle_sync_data_ops g_member_ops = {
.update_bucket_hash = _member_update_bucket_hash,
.get_items = _member_get_items,
.get_page = _member_get_page,
.apply_items = _member_apply_items,
.apply_update = NULL, /* online-статус больше не рассылается через merkle */
.validate_peer = _member_validate_peer,
.finish = member_finish,
};
/* ── Public API ── */
static void _rebuild_all_trees(struct UTUN_INSTANCE* inst) {
sqlite3* db = _db(inst); if (!db) return;
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db,
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers_%'",
-1, &st, NULL) != SQLITE_OK) return;
while (sqlite3_step(st) == SQLITE_ROW) {
const char* tbl = (const char*)sqlite3_column_text(st, 0);
if (!tbl || strncmp(tbl, "peers_", 6) != 0) continue;
const char* ch_id = tbl + 6;
/* полный пересчёт: снести всё дерево канала, затем собрать заново из peers */
sqlite3_stmt* del = NULL;
if (sqlite3_prepare_v2(db, "DELETE FROM merkle_tree_hash WHERE namespace=?", -1, &del, NULL) == SQLITE_OK) {
sqlite3_bind_text(del, 1, ch_id, -1, SQLITE_STATIC);
sqlite3_step(del);
sqlite3_finalize(del);
}
sqlite3_stmt* ns = NULL;
char sql[256]; snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\"", tbl);
if (sqlite3_prepare_v2(db, sql, -1, &ns, NULL) == SQLITE_OK) {
while (sqlite3_step(ns) == SQLITE_ROW) {
uint64_t nid = (uint64_t)sqlite3_column_int64(ns, 0);
merkle_sync_recompute_path(inst, ch_id, nid);
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: rebuild tree ns=%s nid=%016llx", MS_ID, ch_id, (unsigned long long)nid);
static int _rebuild_all_trees(struct UTUN_INSTANCE* inst) {
sqlite3* db = _db(inst);
sqlite3_stmt* tables = NULL;
int rc = sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers_%'", -1, &tables, NULL);
if (rc != SQLITE_OK) goto fail;
if (sqlite3_exec(db, "SAVEPOINT member_rebuild", NULL, NULL, NULL) != SQLITE_OK) goto fail;
while ((rc = sqlite3_step(tables)) == SQLITE_ROW) {
const char* table = (const char*)sqlite3_column_text(tables, 0);
if (!table || strncmp(table, "peers_", 6)) continue;
const char* ns = table + 6;
uint64_t mask = merkle_sync_level_prefix(UINT64_MAX, MT_MAX_LEVEL);
char sql[256];
snprintf(sql, sizeof(sql), "SELECT DISTINCT (node_id & %lld) FROM \"%s\" WHERE source=0", (long long)mask, table);
sqlite3_stmt* leaves = NULL;
int lr = sqlite3_prepare_v2(db, sql, -1, &leaves, NULL);
if (lr == SQLITE_OK) {
while ((lr = sqlite3_step(leaves)) == SQLITE_ROW) {
uint64_t key = (uint64_t)sqlite3_column_int64(leaves, 0);
if (merkle_sync_recompute_path(inst, ns, key) < 0) { lr = SQLITE_ERROR; break; }
}
sqlite3_finalize(ns);
}
sqlite3_finalize(leaves);
if (lr != SQLITE_DONE) { rc = lr; break; }
}
sqlite3_finalize(st);
if (rc != SQLITE_DONE) {
if (sqlite3_exec(db, "ROLLBACK TO member_rebuild", NULL, NULL, NULL) != SQLITE_OK)
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: rebuild rollback failed: %s", MS_ID, sqlite3_errmsg(db));
}
int release = sqlite3_exec(db, "RELEASE member_rebuild", NULL, NULL, NULL);
sqlite3_finalize(tables); tables = NULL;
if (rc != SQLITE_DONE || release != SQLITE_OK) goto fail;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: trees rebuilt from distinct leaves", MS_ID);
return 0;
fail:
sqlite3_finalize(tables);
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: tree rebuild failed: %s", MS_ID, db ? sqlite3_errmsg(db) : "no DB");
return -1;
}
/* Подписаться на BGP node-события chat-группы (BGP → member_sync → node_props_changed). */
@@ -911,11 +1077,11 @@ void member_sync_subscribe_group(struct UTUN_INSTANCE* inst, struct TOPO_GROUP*
int member_sync_init(struct UTUN_INSTANCE* inst) {
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: init — inst is NULL", MS_ID); return -1; }
int rc = merkle_sync_init(inst, 0x31, &g_member_ops, inst);
int rc = merkle_sync_init(inst, ETCP_RT_ID_MEMBER_SYNC, &g_member_ops, inst);
if (rc != 0) return rc;
/* очистка битых записей во всех каналах перед построением дерева */
member_sync_verify_and_purge_all(inst);
_rebuild_all_trees(inst);
if (_rebuild_all_trees(inst) < 0) { merkle_sync_destroy(inst); return -1; }
/* подписка на BGP-события существующих chat-групп */
if (inst->topo_groups && inst->topo_groups->group_list) {
struct ll_entry* ge = inst->topo_groups->group_list->head;
@@ -941,6 +1107,15 @@ void member_sync_destroy(struct UTUN_INSTANCE* inst) {
ge = ge->next;
}
}
/* освободить callback-списки (иначе утекут при chat_core_destroy) */
if (CC(inst)) {
struct ms_props_cbk* pc = CC(inst)->props_cbks;
while (pc) { struct ms_props_cbk* n = pc->next; u_free(pc); pc = n; }
CC(inst)->props_cbks = NULL;
struct ms_apply_cbk* ac = CC(inst)->apply_cbks;
while (ac) { struct ms_apply_cbk* n = ac->next; u_free(ac); ac = n; }
CC(inst)->apply_cbks = NULL;
}
merkle_sync_destroy(inst);
}
@@ -958,85 +1133,6 @@ void member_sync_cancel_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
merkle_sync_cancel_ns(inst, ch_id);
}
/* Сериализует одного мембера в wire-формат [count:2][member...].
Возвращает 0 и *out_len, или -1 если мембер не найден/нет БД. */
static int _serialize_member(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t member_id,
uint8_t* buf, size_t buf_sz, size_t* out_len) {
if (!inst || !ch_id || !buf || !out_len) return -1;
sqlite3* db = _db(inst); if (!db) return -1;
char peers_tbl[128]; _peers_table(ch_id, peers_tbl, sizeof(peers_tbl));
sqlite3_stmt* stmt = NULL;
char sql[512]; snprintf(sql, sizeof(sql),
"SELECT node_id, x25519_pubkey, ed25519_pubkey,"
" join_sig, join_ts, update_sig, update_ts, userinfo, adm_tags, adm_tags_sig,"
" signed_by, signature"
" FROM \"%s\" WHERE node_id=?", peers_tbl);
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: serialize_member — query failed ch=%s nid=0x%016llx", MS_ID, ch_id, (unsigned long long)member_id);
return -1;
}
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)member_id);
if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return -1; }
uint64_t nid = (uint64_t)sqlite3_column_int64(stmt, 0);
const uint8_t* x25 = (const uint8_t*)sqlite3_column_blob(stmt, 1);
const uint8_t* ed = (const uint8_t*)sqlite3_column_blob(stmt, 2);
const uint8_t* sig = (const uint8_t*)sqlite3_column_blob(stmt, 3);
uint64_t jts = (uint64_t)sqlite3_column_int64(stmt, 4);
const uint8_t* usig = (const uint8_t*)sqlite3_column_blob(stmt, 5);
uint64_t uts = (uint64_t)sqlite3_column_int64(stmt, 6);
const char* nm = (const char*)sqlite3_column_text(stmt, 7);
const char* atags = (const char*)sqlite3_column_text(stmt, 8);
const uint8_t* atsig = (const uint8_t*)sqlite3_column_blob(stmt, 9);
uint64_t sb = (uint64_t)sqlite3_column_int64(stmt, 10);
const uint8_t* sgn = (const uint8_t*)sqlite3_column_blob(stmt, 11);
if (!x25 || !ed) { sqlite3_finalize(stmt); return -1; }
uint8_t nl = nm ? (uint8_t)strnlen(nm, 255) : 0;
uint8_t atl = atags ? (uint8_t)strnlen(atags, 255) : 0;
uint8_t flags = (sig && jts) ? PEERS_FLAG_HAS_JOIN : 0;
size_t need = 2 + 8 + 32 + 32 + 1 + (flags & PEERS_FLAG_HAS_JOIN ? 72 : 0)
+ 72 + 1 + (size_t)nl + 1 + (size_t)atl + 64 + 8 + 64;
if (need > buf_sz) { sqlite3_finalize(stmt); return -1; }
size_t off = 0;
uint16_t wcnt = 1; memcpy(buf, &wcnt, 2); off += 2;
memcpy(buf + off, &nid, 8); off += 8;
memcpy(buf + off, x25, 32); off += 32;
memcpy(buf + off, ed, 32); off += 32;
buf[off++] = flags;
if (flags & PEERS_FLAG_HAS_JOIN) { memcpy(buf + off, sig, 64); off += 64; memcpy(buf + off, &jts, 8); off += 8; }
if (usig && uts) { memcpy(buf + off, usig, 64); off += 64; memcpy(buf + off, &uts, 8); off += 8; }
else { memset(buf + off, 0, 72); off += 72; }
buf[off++] = nl; if (nl) { memcpy(buf + off, nm, nl); off += nl; }
buf[off++] = atl; if (atl) { memcpy(buf + off, atags, atl); off += atl; }
if (atsig && sqlite3_column_bytes(stmt, 9) >= 64) { memcpy(buf + off, atsig, 64); }
else { memset(buf + off, 0, 64); }
off += 64;
memcpy(buf + off, &sb, 8); off += 8;
if (sgn && sqlite3_column_bytes(stmt, 11) >= 64) { memcpy(buf + off, sgn, 64); }
else { memset(buf + off, 0, 64); }
off += 64;
sqlite3_finalize(stmt);
*out_len = off;
return 0;
}
void member_sync_broadcast_one(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t member_id) {
uint8_t buf[8192]; size_t off = 0;
if (_serialize_member(inst, ch_id, member_id, buf, sizeof(buf), &off) != 0) return;
merkle_sync_broadcast(inst, ch_id, inst->node_id, buf, off);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: broadcast_one ch=%s nid=0x%016llx size=%zu",
MS_ID, ch_id, (unsigned long long)member_id, off);
}
int member_sync_send_to(struct UTUN_INSTANCE* inst, const char* ch_id,
uint64_t member_id, uint64_t target_peer) {
uint8_t buf[8192]; size_t off = 0;
if (_serialize_member(inst, ch_id, member_id, buf, sizeof(buf), &off) != 0) return -1;
return merkle_sync_send_to(inst, ch_id, target_peer, buf, off);
}
int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id,
uint64_t member_id, const uint8_t* x25519,
@@ -1047,7 +1143,8 @@ int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id,
const char* adm_tags, const uint8_t* adm_tags_sig, int storage,
uint64_t signed_by, const uint8_t* signature) {
if (!inst || !ch_id) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: put — inst=%p ch_id=%s", MS_ID, (void*)inst, ch_id ? ch_id : "(null)"); return -1; }
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: put ch=%s nid=%016llx userinfo=%s adm_tags=%s storage=%d signed_by=%016llx", MS_ID, ch_id, (unsigned long long)member_id, userinfo ? userinfo : "", adm_tags ? adm_tags : "", storage, (unsigned long long)signed_by);
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: put ch=%s nid=%016llx tags_bytes=%zu storage=%d signed_by=%016llx",
MS_ID, ch_id, (unsigned long long)member_id, adm_tags ? strlen(adm_tags) : 0, storage, (unsigned long long)signed_by);
struct ms_member_rec m;
memset(&m, 0, sizeof(m));
@@ -1074,8 +1171,3 @@ int member_sync_count(struct UTUN_INSTANCE* inst, const char* ch_id) {
sqlite3_finalize(stmt);
return c;
}
const uint8_t* member_sync_get_hash(struct UTUN_INSTANCE* inst, const char* ch_id,
uint8_t level, uint64_t prefix64) {
return merkle_sync_get_hash(inst, ch_id, level, prefix64);
}
+24 -60
View File
@@ -1,38 +1,11 @@
/*
* ── member_sync — синхронизация участников чата ──
*
* Тонкая прослойка над merkle_sync, адаптированная под мемберов каналов.
* Данные хранятся в таблицах: nodes, node_addresses, peers_<channel_id>.
* Хеш мембера = SHA256(node_id || x25519 || ed25519 || join_sig || join_ts || update_sig || update_ts || adm_tags_len || adm_tags || adm_tags_sig).
*
* ── Использование ──
*
* // разово: инициализация (из chat_sync_init)
* member_sync_init(inst);
*
* // асинхронный запуск синхронизации (из cs_on_conn_up)
* member_sync_start(inst, peer, ch_id, on_done, ch);
*
* // коллбэк: синхронизация завершена
* static void on_done(uint64_t peer, const char* ch_id, int result, void* arg) {
* struct channel_cache* ch = (struct channel_cache*)arg;
* if (result == MT_OK) ch->synced = CS_SYNC_DONE;
* }
*
* // добавление/обновление мембера (из chat_core_create_channel, cs_handle_welcome)
* member_sync_put(inst, ch_id, node_id, x25519, ed25519,
* join_sig, join_ts, update_sig, update_ts, userinfo,
* adm_tags, adm_tags_sig, storage);
* // дерево автоматически пересчитано
*
* // отмена синхронизации (коллбэк НЕ вызывается)
* member_sync_cancel(inst, peer, ch_id);
*
* // количество мемберов в канале
* int n = member_sync_count(inst, ch_id);
*
* // разово: завершение (из chat_sync_destroy)
* member_sync_destroy(inst);
* member_sync — подписанные записи участников каналов и их обмен через merkle_sync.
* Запись содержит блок участника, блок владельца и подпись дерева приглашений.
* Версии блоков сравниваются отдельно; запись и Merkle-хеш меняются атомарно.
* Локальный put запускает распространение после commit; start ждёт сверки с пиром.
* Участие в CHAT-группе определяется локальной таблицей peers_<channel_id>.
* Сетевые адреса принадлежат topo_node и не входят в блок участника.
* Модуль живёт внутри чат-сервиса; все операции и callbacks — в потоке uasync.
*/
#ifndef MEMBER_SYNC_H
@@ -52,16 +25,22 @@ struct TOPO_GROUP;
struct sqlite3;
/* Результат применения рекорда мембера (compare+update по версиям двух блоков). */
#define MS_APPLY_CHANGED 0x01 /* блок стал новее — обновлён, нужен recompute + relay остальным */
#define MS_APPLY_STALE 0x02 /* у нас версия новее — отправить наш полный рекорд автору */
#define MS_APPLY_CHANGED 0x01 /* запись и дерево обновлены */
#define MS_APPLY_STALE 0x02 /* локальная версия новее; обратный проход доставит её пиру */
/* Распарсенный рекорд мембера (два подписанных блока). */
/* UTF-8 JSON bytes, excluding NUL. A complete record fits in one Merkle page. */
#define MS_ADM_TAGS_MAX 8192
/* Байты для подписи владельца: tags без NUL, затем node_id[8] в порядке байтов хоста.
* Возвращает длину или -1, если данные/буфер не подходят. */
int member_sync_build_owner_msg(uint64_t node_id, const char* tags, uint8_t* out, size_t capacity);
/* Представление записи; все указатели заимствованы на время вызова. */
struct ms_member_rec {
uint64_t node_id;
uint64_t node_id; /* derive(x25519), ключ записи участника */
const uint8_t* x25519; /* 32 байта, обязательно */
const uint8_t* ed25519; /* 32 байта, обязательно */
const uint8_t* join_sig; /* 64 байта, может быть NULL */
uint64_t join_ts;
uint64_t join_ts; /* эпоха вступления, Unix seconds */
const uint8_t* update_sig; /* 64 байта, может быть NULL */
uint64_t update_ts; /* ver блока мембера */
const char* userinfo; /* JSON {"name":...}, может быть NULL */
@@ -105,8 +84,8 @@ void member_sync_remove_apply_cbk(struct UTUN_INSTANCE* inst, member_apply_cbk_f
/*
* Инициализировать модуль: создаёт merkle_sync с коллбэками для мемберов
* (ETCP сервис 0x31), регистрирует _on_node_updated для пересчёта дерева
* при обновлении node info.
* (ETCP_RT_ID_MEMBER_SYNC), проверяет локальные записи, восстанавливает деревья
* и подписывается на события CHAT-групп. 0/-1.
*/
int member_sync_init(struct UTUN_INSTANCE* inst);
@@ -191,7 +170,7 @@ int member_sync_build_join_msg(const uint8_t* ch_x25519, const uint8_t* ch_ed25
* - блок B (владелец): ver = adm_tags.ver, подписан канальным ключом (adm_tags_sig).
* Каждый блок обновляется независимо, только если его ver увеличился.
* Если ver пришедшего блока меньше локального — блок игнорируется и выставляется
* MS_APPLY_STALE (нужно отправить нашу свежую запись автору from_peer).
* MS_APPLY_STALE (локальная версия новее; её доставит обратный проход).
* Возвращает битовую маску MS_APPLY_*, <0 — ошибка.
*/
int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
@@ -212,28 +191,13 @@ int member_sync_verify_and_purge(struct UTUN_INSTANCE* inst, const char* ch_id)
/* Прогнать очистку битых записей по всем каналам (при init). */
void member_sync_verify_and_purge_all(struct UTUN_INSTANCE* inst);
/*
* Отправить наш полный рекорд мембера конкретному пиру (send-back при stale).
* Возвращает 0 при успехе, -1 если нет сессии/соединения.
*/
int member_sync_send_to(struct UTUN_INSTANCE* inst, const char* ch_id,
uint64_t member_id, uint64_t target_peer);
/* Количество мемберов в канале (SELECT COUNT из peers_<ch_id>). */
int member_sync_count(struct UTUN_INSTANCE* inst, const char* ch_id);
/* Сериализовать одного мембера и отправить всем подключённым пирам через merkle_sync_broadcast.
Вызывается после локального изменения данных (adm_tags, имя, адреса). */
void member_sync_broadcast_one(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t member_id);
/* Получить хеш бакета из merkle_tree_hash (для тестов). */
const uint8_t* member_sync_get_hash(struct UTUN_INSTANCE* inst,
const char* ch_id, uint8_t level, uint64_t prefix64);
/* Количество мемберов в канале. */
int member_sync_count(struct UTUN_INSTANCE* inst, const char* ch_id);
/*
* Коллбэк: изменились adm_tags любого узла (включая себя).
* channel_id — канал (namespace), в котором произошло изменение.
* Вызывается при успешной обработке MSG_ITEM_UPDATE с adm_tags.
* Уведомляет об изменении свойств после commit и при обновлении состояния узла в BGP.
* Многоподписочный — можно добавить несколько подписчиков.
*/
typedef void (*node_props_changed_fn)(uint64_t node_id, const char* adm_tags, const char* channel_id, void* arg);
+46 -134
View File
@@ -1,149 +1,61 @@
# member_sync — синхронизация участников канала
# member_sync — адаптер участников каналов
## 1. Назначение
Поверх [merkle_sync](merkle_sync_doc.md) синхронизирует source=0 записи таблиц peers_<channel_id>.
Транспортный движок не занимается подписями и версиями участников.
Тонкая прослойка над `merkle_sync`, адаптированная под участников (мемберов) чат-каналов.
Хранит мемберов в SQLite (`peers_<ch_id>`, `nodes`, `node_addresses`), пересчитывает Merkle-дерево
при каждом изменении и через merkle_sync рассылает дельту другим узлам.
## Использование
Данные мембера защищены двумя независимо подписанными блоками:
- **Блок A (мембер)** — идентичность и профиль: `node_id`, `x25519`, `ed25519`, `userinfo` (имя),
подпись `update_sig` **ключом самого мембера** (`ed25519`); `ver = update_ts` внутри эпохи `join_ts`.
- **Блок B (владелец)** — атрибуты, управляемые владельцем канала: `adm_tags` (JSON с `ver`,
`storage`), подпись `adm_tags_sig` **канальным ключом** (`ed25519` канала); `ver = adm_tags.ver`.
- member_sync_init вызывается при инициализации chat_sync и перестраивает производный индекс.
- member_sync_start подключает канал к пиру и запрашивает подтверждение совпадения данных.
- member_sync_put изменяет локальную запись, атомарно обновляет индекс и после commit уведомляет движок.
- Прямые broadcast/send_to не нужны: локальная версия возвращается второму узлу при обратном проходе.
- cancel/destroy снимают ожидания; отдельного публичного API рассылки нет.
Оба блока обновляются независимо — только если их `ver` увеличился. Устаревший блок помечается
stale и автору отправляется свежая запись (send-back).
## Записи и применение
## 2. Как пользоваться
У записи два независимо версионируемых блока: участник (join_ts, update_ts) и владелец (adm_tags.ver).
Удаление используется для повреждённых записей. Конфликт одинаковых версий с разным содержимым
не разрешается порядком доставки: отсутствие прогресса завершает Merkle-раунд ошибкой конфликта.
```c
// ── разово (из chat_sync_init) ──
member_sync_init(inst); // merkle_sync_init(0x31) + verify/purge + rebuild + подписка
Входящая страница сначала целиком проверяется на корректность раскладки, длин и строк.
Затем записи применяются под savepoint member_page. Ошибка откатывает всю страницу вместе с индексом.
Уведомления GUI/топологии и changed выполняются после commit.
Каждый member_sync_apply_record также имеет savepoint, поэтому ошибка пересчёта не оставляет запись без индекса.
При внешней транзакции вызывающая сторона отвечает за changed после commit.
// ── подписка на BGP-события канала (BGP → member_sync → node_props_changed) ──
member_sync_subscribe_group(inst, group); // при создании канала
Проверяются привязка node_id к X25519, доверенный Ed25519, подписи блока участника и владельца.
Для дерева приглашений родитель может прийти позже ребёнка; finish повторно проверяет зависимости
перед подтверждением корня. Это не отдельная карантинная таблица: промежуточные записи существуют в БД
до finish, поэтому MT_OK нельзя заменять фактом их присутствия.
// ── запуск синхронизации (из cs_on_conn_up) ──
member_sync_start(inst, peer, ch_id, on_done, ch);
## Канонический хеш
// ── коллбэк завершения ──
static void on_done(uint64_t peer, const char* ch_id, int result, void* arg) {
if (result == MT_OK) ch->synced = CS_SYNC_DONE;
}
SHA256 по полям:
node_id:8, x25519:32, ed25519:32, join_sig:64, join_ts:8,
update_sig:64, update_ts:8, userinfo_len:1, userinfo,
adm_tags_len:2, adm_tags, adm_tags_sig:64, signed_by:8, signature:64.
// ── добавление/обновление мембера (локальное, без верификации подписей) ──
member_sync_put(inst, ch_id, node_id, x25519, ed25519,
join_sig, join_ts, update_sig, update_ts, userinfo,
adm_tags, adm_tags_sig, storage);
Целые big-endian; отсутствующие подписи представлены нулями.
userinfo ограничен 255 байтами, adm_tags — 8192 байтами UTF-8 без NUL. Длина adm_tags — uint16 big-endian.
Хеш и подпись владельца покрывают весь adm_tags без обрезания. Хеш включает userinfo и дерево приглашений.
Формат сообщений, подписываемых моделью, определяется build_join_msg/build_update_msg/build_sign_msg отдельно.
// ── онлайн-статус: только локальная запись, возвращает 1 если изменилось ──
if (member_sync_set_online(inst, node_id, 1))
merkle_sync_push_update(inst, ch_id, node_id, 0x01, (uint8_t[]){1}, 1); // рассылает caller
## Страница на проводе
// ── отмена (коллбэк не вызовется) ──
member_sync_cancel(inst, peer, ch_id);
Начинается с count:2 big-endian. Далее count записей:
node_id:8, x25519:32, ed25519:32, flags:1,
при HAS_JOIN — join_sig:64 и join_ts:8,
update_sig:64, update_ts:8, userinfo_len:1, userinfo,
adm_tags_len:2, adm_tags, adm_tags_sig:64, signed_by:8, signature:64.
// ── количество мемберов ──
int n = member_sync_count(inst, ch_id);
Целые big-endian. Встроенные NUL и хвостовые байты запрещены.
get_page перечисляет только один лист, в возрастающем порядке node_id, строго после курсора.
SQLite использует signed INTEGER; внутри одного листа знак ключей одинаков, поэтому SQL-порядок совпадает с unsigned.
Следующая запись, не помещающаяся в буфер, остаётся для следующей страницы.
Ошибки SQL и повреждённые размеры BLOB возвращаются вызывающему коду.
// ── разово (из chat_sync_destroy) ──
member_sync_destroy(inst);
```
## Диагностика
## 3. Модель данных
- **Таблицы**: `peers_<ch_id>` (per-channel мемберы), `nodes`/`node_addresses` (глобальная
идентичность и адреса, обновляются отдельно через `topo_node_sqlite_*`).
- **Хеш мембера** (участвует в Merkle-дереве):
`SHA256(node_id || x25519 || ed25519 || join_sig || join_ts || update_sig || update_ts || adm_tags_len || adm_tags || adm_tags_sig)`.
Поля `userinfo` (имя) и `source` в хеш **не** входят.
- **Колонка `source`**: `source != 0` — плейсхолдер, созданный topo_group (не верифицирован);
такие записи **исключаются** из хеша и сериализации. `source == 0` — обычный верифицированный
меркл-рекорд.
### Wire-формат мембера (get_items / apply_items / _serialize_member)
```
[count:2][node_id:8][x25519:32][ed25519:32][flags:1]
(flags&HAS_JOIN → [join_sig:64][join_ts:8])
[update_sig:64][update_ts:8]
[userinfo_len:1][userinfo]
[adm_tags_len:1][adm_tags][adm_tags_sig:64]
```
`flags = PEERS_FLAG_HAS_JOIN` — `join_sig`/`join_ts` присутствуют (join-запись).
## 4. apply_record — по-блочное сравнение и применение
`member_sync_apply_record(inst, ch_id, from_peer, &rec)` — единая точка записи мембера
(и локальная `put`, и приёмная). Возвращает битовую маску `MS_APPLY_CHANGED | MS_APPLY_STALE`.
1. **Идентичность** (только для приёмных записей): `node_id == sc_derive_node_id_from_pubkey(x25519)`
и `ed25519` совпадает с доверенным из `nodes`. Несовпадение → forgery, блок A отклоняется.
2. **Блок A** — проверка `update_sig` (или `join_sig` для join-only). Локальная запись
(`from_peer == inst->node_id`) доверенная, без верификации. Сравнение `ver`:
- нет локальной записи → changed;
- `join_ts > local_join_ts` → changed (**re-join / смена ключа**);
- `join_ts < local_join_ts` → stale (старая эпоха);
- иначе по `update_ts`.
3. **Блок B** — проверка `adm_tags_sig` канальным ключом, `adm_storage = (storage=="yes")`.
Сравнение `adm_tags.ver` с локальным.
4. **Запись**: changed-блоки пишутся (`member_block_put` / `member_owner_put`), узел обновляется
через `node_update_verified`, пересчитывается классификация `nodeinfo_updated` (из
`node_addresses`, полученных по BGP), затем `merkle_sync_recompute_path`.
5. **Реальная сверка**: если `recompute_path` вернул 0 (данные идентичны) — `changed` сбрасывается
в 0 (no-op), чтобы не рассылать.
На приёме (`_member_apply_items`) каждый `MS_APPLY_CHANGED` генерирует `node_props_changed` и
включается в relay (`merkle_sync_broadcast`); `MS_APPLY_STALE` вызывает `member_sync_send_to`
(отправить нашу свежую запись автору).
## 5. Подписки и коллбэки
- **`member_sync_subscribe_group(inst, group)`** — подписывает на BGP node-события chat-группы
(`topo_group_add_node_cbk`). При событии BGP (`_ms_on_bgp_node`) рассылается `node_props_changed`
с текущим `adm_tags`. REMOVE **не удаляет** мембера.
- **`member_sync_add_props_cbk/remove_props_cbk`** — многоподписочный `node_props_changed_fn`
(изменились `adm_tags` любого узла, включая себя).
- **`member_sync_set_node_updated_cb`** — одиночный `member_sync_node_updated_fn` (online-статус
узла изменился по инициативе удалённого пира, вызывается из uasync-потока).
## 6. Верификация и удаление
Единственный механизм удаления мембера — битая запись:
- `member_sync_verify_local_record` — проверяет привязку `node_id = derive(x25519)`, подписи
`update_sig`/`join_sig`/`adm_tags_sig`. Отсутствие подписи = «не верифицировано» → **не удаляем**.
- `member_sync_verify_and_purge` / `_all` — прогоняют записи канала (всех `peers_%`), битые удаляют
через `topo_node_sqlite_member_del` + `recompute_path`.
- Вызываются при `member_sync_init` и при старте синхронизации канала. Отдельного
`member_sync_del` нет — conn_down ≠ удаление мембера.
## 7. API
| Функция | Назначение |
|---|---|
| `member_sync_init(inst)` | `merkle_sync_init(0x31, &g_member_ops, inst)` + verify/purge + rebuild всех деревьев + подписка на существующие chat-группы |
| `member_sync_destroy(inst)` | Отписка от групп, `merkle_sync_destroy` |
| `member_sync_subscribe_group(inst, group)` | Подписка на BGP-события CHAT-группы |
| `member_sync_start(inst, peer, ch_id, done_cb, arg)` | `verify_and_purge` + `merkle_sync_start` |
| `member_sync_cancel(inst, peer, ch_id)` | `merkle_sync_cancel` |
| `member_sync_put(...)` | Локальная запись мембера (trusted) → `apply_record` |
| `member_sync_apply_record(inst, ch_id, from_peer, &rec)` | Приём/применение рекорда (по-блочно) |
| `member_sync_send_to(inst, ch_id, member_id, target_peer)` | Send-back нашей свежей записи конкретному пиру |
| `member_sync_broadcast_one(inst, ch_id, member_id)` | Сериализовать мембера и разослать всем (relay после локального изменения) |
| `member_sync_set_online(inst, node_id, online)` | Записать `nodes.online`. Возвращает 1 (изменилось) / 0 |
| `member_sync_count(inst, ch_id)` | `COUNT(*)` из `peers_<ch_id>` |
| `member_sync_build_join_msg(...)` / `build_update_msg(...)` | Канонические сообщения для подписи/верификации join/update (112 байт и 64+8+userinfo) |
| `member_sync_get_hash(inst, ch_id, level, prefix64)` | Хеш бакета (для тестов) |
| `member_sync_add/remove_props_cbk`, `set_node_updated_cb` | Подписки на изменения |
## 8. Нюансы
- **Онлайн-статус не участвует в Merkle-дереве**: не хешируется и не версионируется. `set_online`
только пишет в БД и возвращает 1/0; рассылку `MSG_ITEM_UPDATE` выполняет **caller** (chat_sync).
- **`member_sync_put` = локальный `apply_record`** с `from_peer = inst->node_id` — без верификации
подписей (свои данные доверенные).
- **`source`-плейсхолдеры** topo_group пропускаются (`src != 0`), в дереве не участвуют.
- **Re-join / смена ключа** распознаётся по `join_ts` (перекрывает сравнение `update_ts`).
- Категория логов — `DEBUG_CATEGORY_MEMBER_SYNC`.
Категория member_sync: ошибки содержат канал, node_id и причину отказа.
DEBUG показывает применение версий и размеры страниц, TRACE — входы и расчёт листа.
Хеши родителей вычисляет merkle_tree; адаптер никогда не перечисляет все записи для внутреннего узла.
+535 -838
View File
File diff suppressed because it is too large. Load diff
+62 -294
View File
@@ -1,313 +1,81 @@
/* merkle_sync — обмен различиями между наборами записей по Merkle-дереву.
* Движок сверяет хеши и передаёт страницы изменившихся листьев; модель данных
* через data_ops читает записи, проверяет подписи/права и сохраняет изменения.
* Сессия относится к (пир, namespace), отправка учитывает backpressure.
* Все операции — в потоке uasync; один движок на UTUN_INSTANCE. */
#ifndef MERKLE_SYNC_H
#define MERKLE_SYNC_H
#include <stdint.h>
#include "merkle_tree.h"
#include <stddef.h>
#include <openssl/evp.h>
struct UTUN_INSTANCE;
#define MT_MAX_LEVEL 5
#define MT_BUCKETS 32
#define MT_HASH_SIZE 32
/*
* ── Архитектура ──
*
* merkle_sync — универсальный протокол синхронизации на Merkle-деревьях.
* Группирует элементы (items) в префиксное дерево: 5 уровней × 32 бакета,
* SHA256-хеши. Два пира обмениваются хешами уровней, находят различающиеся
* бакеты и передают только их содержимое.
*
* merkle_sync не знает, что такое "элемент" — потребитель (member_sync)
* предоставляет четыре коллбэка, описывающих модель данных:
*
* update_bucket_hash — перечислить элементы в диапазоне ключей,
* вычислить хеш каждого и подать в SHA256-контекст
* get_items — сериализовать элементы бакета в wire-формат
* apply_items — десериализовать и сохранить полученные элементы
* apply_update — применить лёгкое обновление (MSG_ITEM_UPDATE)
*
* Потребитель (chat_sync/chat_core) работает только с member_sync
* и не видит merkle_sync напрямую:
*
* // ── разово ──
* member_sync_init(inst);
*
* // ── запустил синхронизацию — забыл ──
* member_sync_start(inst, peer, ch_id, on_done, my_ctx);
*
* // ── получил результат ──
* static void on_done(uint64_t peer, const char* ch_id, int result, void* arg) {
* if (result == MT_OK) printf("sync ok\n");
* if (result == MT_ERR_TIMEOUT) printf("timeout\n");
* }
*
* // ── изменил данные — дерево само пересчиталось ──
* member_sync_put(inst, ch_id, node_id, x25519, ed25519, join_sig, addrs, ac);
*
* // ── отменил — коллбэк не вызовется ──
* member_sync_cancel(inst, peer, ch_id);
*
* // ── разово ──
* member_sync_destroy(inst);
*
* ── Формат дерева ──
*
* 5-уровневое префиксное дерево над 64-битными ключами (node_id).
* Каждый уровень берёт 5 старших бит ключа: уровень 1 — биты 59-63,
* уровень 5 — биты 39-63. Каждый узел дерева разбивается на 32 бакета
* (по 5 бит = 32 комбинации).
*
* level 1 [0..31] корень: 32 бакета по 5 бит
* level 2 [0..31]...[0..31] каждый — ещё 32 бакета
* ...
* level 5 [0..31].........[0..31] листья: 32^5 = 33M бакетов макс
*
* Хеш бакета = SHA256(хеш_элемента_1 || ... || хеш_элемента_N).
* Бакет считается терминальным (leaf) на уровне 5 или если в нём < 8 элементов.
*
* ── Wire-протокол (сервис ETCP, id задаётся при init) ──
*
* Каждое сообщение: [svc_id:1][ns_len:1][ns:var][type:1][payload:var]
*
* MSG_HASHES (0x01): level,prefix,is_data, [bitmap+hashes | member_data]
* MSG_REQUEST (0x02): count, [level,prefix,is_terminal]*
* MSG_BATCH (0x03): count, [level,prefix,is_terminal,[member_data|bitmap+hashes]]*
*
* Алгоритм:
* A → MSG_HASHES(level=1, bitmap+hashes всех 32 бакетов уровня 2) → B
* B сравнивает со своим деревом, находит различающиеся бакеты
* B → MSG_REQUEST(level=2, prefix=X, is_terminal) → A
* A → MSG_BATCH(данные бакета) → B
* B сохраняет, пересчитывает хеши
* Рекурсивно для подбакетов, пока хеши не совпадут.
*
* ── Сессии ──
*
* Протокол работает поверх надёжного транспорта (ETCP).
* Таймаутов и ретраев нет — при ошибке сессия завершается.
*/
/* ── Data model callbacks ── */
#define MT_OK 0
#define MT_ERR_IO -1
#define MT_ERR_PROTOCOL -2
#define MT_ERR_DATA -3
#define MT_ERR_CONFLICT -4
#define MT_ERR_DISCONNECTED -5
#define MT_ERR_TIMEOUT -6
#define MT_PAGE_SIZE 16384
/* Namespace — каноническая десятичная строка uint64_t. Движок не знает CHAT-групп.
* SHA256 листа обновляется моделью в порядке unsigned key; родителей считает merkle_tree.
* Все операции выполняются в uasync-потоке экземпляра. */
struct merkle_sync_data_ops {
/*
* Подать хеши всех элементов в префиксном диапазоне в sha_ctx.
*
* ctx — data_ctx, переданный в merkle_sync_init
* ns — namespace (например channel_id)
* level — уровень дерева (1..5)
* prefix64 — префикс ключа (старшие level*5 бит, остальные нули)
* sha_ctx — уже инициализирован (EVP_DigestInit_ex), потребитель
* делает только EVP_DigestUpdate(sha_ctx, item_hash, 32)
* для каждого элемента
*
* Возвращает количество элементов (0 = бакет пуст, -1 = ошибка).
*
* Пример реализации для мемберов:
* SELECT ... FROM peers_<ns> JOIN nodes
* WHERE (node_id & mask) == prefix64 ORDER BY node_id
* для каждой строки: _compute_member_hash() → EVP_DigestUpdate()
*/
int (*update_bucket_hash)(void* ctx, const char* ns, uint8_t level,
uint64_t prefix64, EVP_MD_CTX* sha_ctx);
/*
* Сериализовать элементы бакета в wire-формат.
*
* buf — буфер для записи (выделяет merkle_sync, мин. 64K)
* *len — [in] размер буфера, [out] записанный размер
*
* Wire-формат для мемберов:
* [count:2][node_id:8][x25519:32][ed25519:32][flags:1]([join_sig:64][join_ts:8])[update_sig:64][update_ts:8][userinfo_len:1][userinfo:var][adm_tags_len:1][adm_tags:var][adm_tags_sig:64]...
*
* Возвращает 0 при успехе, <0 при ошибке, -2 если буфер мал.
*/
int (*get_items)(void* ctx, const char* ns, uint8_t level,
uint64_t prefix, uint8_t pbytes, uint8_t* buf, size_t* len);
/*
* Десериализовать и сохранить элементы из wire-формата.
*
* data, len — данные в том же формате, что выдаёт get_items
*
* Вызывается при приёме MSG_HASHES(is_data=1) или MSG_BATCH(is_terminal=1).
* Должна сохранить элементы в БД и вызвать merkle_sync_recompute_path
* для каждого изменённого ключа (через member_sync_put).
*
* Возвращает 0 при успехе, <0 при ошибке.
*/
int (*apply_items)(void* ctx, const char* ns, uint64_t from_peer,
const uint8_t* data, size_t len);
/*
* Применить лёгкое обновление (не влияющее на Merkle-хеш).
* Вызывается при приёме MSG_ITEM_UPDATE(0x04) — online-статус и т.п.
*
* ns — namespace (channel_id)
* key — node_id изменившегося узла
* type — тип обновления (0x01 = online-статус)
* data,len — данные обновления (type=0x01: [online:1])
*
* Возвращает 0 при успехе, <0 при ошибке.
*/
int (*apply_update)(void* ctx, const char* ns, uint64_t key,
uint8_t type, const uint8_t* data, size_t len);
/*
* Проверить, что пир авторизован для namespace ns (напр. его pubkey есть
* в таблице мемберов канала). Вызывается в _recv_cb для КАЖДОГО входящего
* пакета до диспатча. Возвращает 1 если разрешено, 0 если нет.
* Может быть NULL — тогда проверка пропускается (generic-потребитель).
*/
int (*validate_peer)(void* ctx, const char* ns, uint64_t peer);
merkle_leaf_hash_fn update_bucket_hash; // хеш содержимого одного листа
/* Одна страница листа: after_valid=0 начинает обход, иначе только key > after.
* len: ёмкость -> размер; next — последний ключ; more — есть продолжение.
* Пустой результат разрешён только с more=0. Ошибка не кодируется пустой страницей. */
int (*get_page)(void* ctx, const char* ns, uint64_t prefix, int after_valid, uint64_t after,
uint8_t* buf, size_t* len, uint64_t* next, int* more);
/* Проверить весь формат, применить записи и обновить дерево в одной DB-транзакции.
* <0 — ошибка. Никаких broadcast/send-back из этого callback. */
int (*apply_items)(void* ctx, const char* ns, uint64_t from_peer, const uint8_t* data, size_t len);
int (*validate_peer)(void* ctx, const char* ns, uint64_t peer); // необязательная проверка: только 1 разрешает приём
/* Проверить отложенные зависимости модели после обоих проходов, до подтверждения корня. */
int (*finish)(void* ctx, const char* ns);
/* Необязательный отложенный приём: все четыре callback задаются вместе.
* stage_page накапливает страницы без изменения опубликованных хешей;
* commit_pull применяет результат; abort_pull освобождает pull и после успеха. */
int (*begin_pull)(void* ctx, const char* ns, uint64_t peer, uint64_t round, void** pull);
int (*commit_pull)(void* ctx, void* pull);
void (*abort_pull)(void* ctx, void* pull);
int (*stage_page)(void* ctx, void* pull, uint64_t prefix, int after_valid, uint64_t after,
uint64_t next, int more, const uint8_t* data, size_t len);
};
/*
* Коллбэк завершения синхронизации.
*
* peer — node_id пира
* ns — namespace (channel_id)
* result — MT_OK (0) = успех, MT_ERR_TIMEOUT (-1) = зарезервировано (в протоколе не возникает)
* arg — пользовательский контекст
*
* При отмене через merkle_sync_cancel() коллбэк НЕ вызывается.
*/
#define MT_OK 0
#define MT_ERR_TIMEOUT -1
/* result — MT_OK или MT_ERR_*; ns заимствован на время callback. */
typedef void (*merkle_sync_done_cb)(uint64_t peer, const char* ns, int result, void* arg);
/* ── Lifecycle ── */
/*
* Инициализировать модуль. Создаёт таблицу merkle_tree_hash, биндит
* ETCP-сервис svc_id (напр. 0x31), запускает фоновую проверку (bg_timer).
* Вызывается один раз, обычно из member_sync_init().
*
* inst — экземпляр uTun (нужен для etcp_send, uasync, sqlite3)
* svc_id — ID сервиса на ETCP-роутере
* ops — коллбэки модели данных (update_bucket_hash, get_items, apply_items)
* data_ctx — прозрачный контекст, передаваемый в коллбэки первым аргументом
*
* Возвращает 0 при успехе, -1 при ошибке.
*/
int merkle_sync_init(struct UTUN_INSTANCE* inst, uint8_t svc_id,
const struct merkle_sync_data_ops* ops, void* data_ctx);
/*
* Завершить модуль. Отвязывает ETCP-сервис, останавливает bg_timer,
* удаляет все активные сессии (коллбэки НЕ вызываются).
*/
/* Зарегистрировать сервис; нужны ua и общая SQLite. ops/data_ctx живут до destroy. 0/-1. */
int merkle_sync_init(struct UTUN_INSTANCE* inst, uint8_t svc_id,
const struct merkle_sync_data_ops* ops, void* data_ctx);
/* Отменить сессии без done callbacks, снять обработчики и освободить движок. */
void merkle_sync_destroy(struct UTUN_INSTANCE* inst);
/* ── Async sync ── */
/*
* Запустить синхронизацию namespace ns с пиром peer.
* Отправляет MSG_HASHES(level=0). Таймаута нет — сессия завершается при
* совпадении корневого хеша либо удаляется вручную (cancel/destroy).
*
* Если сессия для (peer, ns) уже существует — перезапускает её
* с новым коллбэком (старый коллбэк теряется без вызова).
*
* Возвращает 0 при успехе, -1 при ошибке.
*/
int merkle_sync_start(struct UTUN_INSTANCE* inst, uint64_t peer,
const char* ns, merkle_sync_done_cb done_cb, void* arg);
/*
* Отменить синхронизацию. Удаляет сессию, done_cb НЕ вызывается.
* Безопасно вызывать, если сессия не существует.
*/
/* Подключить namespace к текущему ETCP-соединению и запросить контрольную точку.
* Успех — подтверждение одинакового корня конкретного раунда, не вечный статус.
* Повторные запросы объединяются; каждый callback вызывается один раз, обход не сбрасывается.
* После первого start изменения дерева автоматически синхронизируются с этим пиром.
* <0: запрос не принят, callback не вызывается. DOWN/DELETE завершают запросы ошибкой.
* Явные cancel/destroy освобождают запросы без callback.
* Callback может отменить сессию; уничтожение всего экземпляра должно быть отложенным. */
int merkle_sync_start(struct UTUN_INSTANCE* inst, uint64_t peer, const char* ns, merkle_sync_done_cb cb, void* arg);
void merkle_sync_cancel(struct UTUN_INSTANCE* inst, uint64_t peer, const char* ns);
/*
* Отменить и удалить ВСЕ сессии для namespace ns (независимо от пира).
* Используется при локальном удалении канала. done_cb НЕ вызывается.
*/
/* Отменить все сессии namespace / пира без callbacks. */
void merkle_sync_cancel_ns(struct UTUN_INSTANCE* inst, const char* ns);
void merkle_sync_cancel_peer(struct UTUN_INSTANCE* inst, uint64_t peer);
/* ── Recompute tree path after data change ── */
/* Обновление индекса вызывается моделью данных.
* При внешней транзакции caller вызывает changed ПОСЛЕ commit.
* Без внешней транзакции пересчёт сам создаёт savepoint и уведомляет после commit.
* Возвращает 1=изменилось, 0=no-op, <0=ошибка. */
int merkle_sync_recompute_path(struct UTUN_INSTANCE* inst, const char* ns, uint64_t key);
/* После commit уведомить сессии namespace о локальном изменении дерева. */
void merkle_sync_changed(struct UTUN_INSTANCE* inst, const char* ns);
/* Прочитать хеш узла дерева в hash. 0=успех, <0=ошибка. */
int merkle_sync_read_hash(struct UTUN_INSTANCE* inst, const char* ns, uint8_t level, uint64_t prefix, uint8_t hash[MT_HASH_SIZE]);
/*
* Пересчитать Merkle-дерево для ключа key — все уровни от 1 до 5.
* Вызывается после изменения данных: merkle_sync сам не знает,
* когда данные изменились — потребитель должен вызвать явно.
* member_sync делает это внутри put/del.
*
* Возвращает 1 если хеш хотя бы одного бакета изменился (данные реально
* изменились), 0 если всё совпало, -1 при ошибке.
*/
int merkle_sync_recompute_path(struct UTUN_INSTANCE* inst, const char* ns, uint64_t key);
/*
* Отправить лёгкое обновление всем synced-пирам в namespace ns.
* Не влияет на Merkle-дерево — используется для online-статуса и т.п.
*
* ns — namespace (channel_id)
* key — node_id изменившегося узла
* type — тип обновления (0x01 = online-статус)
* data,len — данные обновления
*/
void merkle_sync_push_update(struct UTUN_INSTANCE* inst, const char* ns,
uint64_t key, uint8_t type, const uint8_t* data, size_t len);
/*
* Переслать данные всем SYNCED-сессиям (кроме from_peer) в namespace ns.
* Используется для relay: когда apply_items обнаружил реальные изменения,
* потребитель вызывает эту функцию чтобы разослать дельту остальным пирам.
*
* data, len — wire-формат [count:2][key:8][val:4]... как от get_items.
*/
void merkle_sync_broadcast(struct UTUN_INSTANCE* inst, const char* ns,
uint64_t from_peer, const uint8_t* data, size_t len);
/*
* Отправить данные одного мембера конкретному пиру (а не всем).
* Используется для "send back" при обнаружении stale-версии.
* data, len — wire-формат [count:2][key:8][val:4]... как от get_items.
* Возвращает 0 при успехе, -1 если нет сессии/соединения к пиру.
*/
int merkle_sync_send_to(struct UTUN_INSTANCE* inst, const char* ns,
uint64_t peer, const uint8_t* data, size_t len);
/* ── Background consistency check ── */
/*
* Проверить консистентность одного случайного level-5 бакета в ns.
* Пересчитывает хеш из данных, сравнивает с сохранённым в merkle_tree_hash.
* При расхождении — пересчитывает весь путь вверх до корня.
* Возвращает 1 если были изменения, 0 если совпало, -1 ошибка.
*/
int merkle_sync_bg_check(struct UTUN_INSTANCE* inst, const char* ns);
/* ── Test helper ── */
/*
* Получить сохранённый хеш бакета из merkle_tree_hash.
* Возвращает указатель на статический буфер (32 байта), перезаписывается
* при следующем вызове. Если бакет не найден — возвращает нулевой хеш.
*/
const uint8_t* merkle_sync_get_hash(struct UTUN_INSTANCE* inst,
const char* ns, uint8_t level, uint64_t prefix64);
/* ── Prefix arithmetic (pure, exported for convenience) ── */
/*
* Выделить level*5 старших бит из 64-битного ключа.
* Уровень 1: биты 59-63 (сдвиг 59)
* Уровень 5: биты 39-63 (сдвиг 39)
* Пример: merkle_sync_level_prefix(0x1234567890ABCDEF, 1) = 0x1000000000000000
*/
uint64_t merkle_sync_level_prefix(uint64_t key, uint8_t level);
/*
* Количество байт, необходимое для хранения префикса уровня level.
* Уровень 1: 1 байт (5 бит), уровень 4: 3 байта (20 бит), уровень 5: 4 байта (25 бит).
*/
uint8_t merkle_sync_prefix_bytes(uint8_t level);
#endif /* MERKLE_SYNC_H */
#endif
+79 -137
View File
@@ -1,158 +1,100 @@
# merkle_sync — синхронизация данных на Merkle-деревьях
# merkle_sync — протокол 2
## 1. Назначение
## Разделение ответственности
Универсальный движок синхронизации между двумя узлами. Группирует элементы в префиксное
Merkle-дерево (5 уровней × 32 бакета, SHA256). Два пира обмениваются хешами уровней, находят
различающиеся бакеты и передают **только их содержимое** — вместо полной пересылки всех данных.
- merkle_tree: производный индекс SQLite, хеши листьев и родителей.
- merkle_sync: обмен, backpressure и жизненный цикл сессий.
- member_sync: формат записей, подписи, версии, применение страниц.
merkle_sync **не знает**, что такое «элемент». Потребитель (member_sync) предоставляет набор
коллбэков `merkle_sync_data_ops`, описывающих модель данных (как перечислить элементы бакета
и посчитать хеш, как сериализовать элементы в wire-формат, как применить полученные).
Движок не знает CHAT-групп. Namespace — каноническая десятичная строка uint64_t.
Все операции выполняются в одном потоке uasync экземпляра.
Работает поверх надёжного транспорта (ETCP), поэтому **таймаутов и ретраев в протоколе нет**:
сессия завершается при совпадении корневого хеша, либо удаляется вручную (`cancel`/`destroy`).
## Дерево и транзакции
## 2. Как пользоваться
Корень имеет уровень 0. Уровни 1–5 разбивают старшие 25 бит ключа на группы по 5 бит.
SHA256 листа: `0x4d || level:1 || item_hashes`, записи упорядочены по unsigned key.
SHA256 родителя: `0x4d || level:1 || child_hash[0] || ... || child_hash[31]`.
Хеш пустого поддерева — 32 нулевых байта; строка индекса для него отсутствует.
Пересчитывается один лист и его предки, до первого неизменившегося хеша.
Напрямую merkle_sync обычно не используют — его потребляет member_sync. Типовой сценарий
потребителя выглядит так:
Индекс создаётся заново при init и заполняется моделью из исходных записей.
Модель меняет данные и вызывает recompute_path в одной транзакции, затем changed после commit.
Без внешней транзакции recompute_path создаёт savepoint и уведомляет самостоятельно.
Ошибки не заменяются пустыми страницами или нулевыми хешами.
```c
// ── разово: инициализация (из member_sync_init) ──
merkle_sync_init(inst, 0x31, &ops, data_ctx); // svc_id — ID ETCP-сервиса
## API
// ── запустил синхронизацию — забыл ──
merkle_sync_start(inst, peer, ns, on_done, my_arg);
init принимает service и data_ops: update_bucket_hash, get_page, apply_items,
необязательные validate_peer и finish.
update_bucket_hash добавляет хеши одного листа в переданный SHA256-контекст.
get_page получает ёмкость буфера и курсор; возвращает размер, последний ключ и флаг продолжения.
apply_items проверяет весь формат и применяет страницу вместе с индексом атомарно.
finish проверяет отложенные зависимости модели перед подтверждением корня.
// ── коллбэк завершения ──
static void on_done(uint64_t peer, const char* ns, int result, void* arg) {
if (result == MT_OK) /* хеши сошлись, данные идентичны */;
if (result == MT_ERR_TIMEOUT) /* (зарезервировано, в протоколе не возникает) */;
}
start подключает namespace к текущему соединению и запрашивает контрольную точку.
Повторные start объединяются, обход не сбрасывается, каждый принятый callback вызывается один раз.
После подключения изменения синхронизируются автоматически. Broadcast/send-back не требуются.
MT_OK означает совпадение корней конкретного раунда, а не отсутствие будущих изменений.
// ── изменил данные — дерево само пересчиталось ──
int changed = merkle_sync_recompute_path(inst, ns, key); // 1 = изменилось, 0 = no-op
cancel, cancel_ns, cancel_peer и destroy освобождают запросы без callback.
DOWN/DELETE/REINIT завершают запросы ошибкой и освобождают сессию.
После восстановления соединения интеграционный слой повторяет start.
Callback может отменить сессию; уничтожение всего экземпляра следует отложить через call_soon.
// ── рассылка дельты остальным пирам (relay) ──
merkle_sync_broadcast(inst, ns, from_peer, data, len);
merkle_sync_push_update(inst, ns, key, type, data, len); // лёгкое обновление (online и т.п.)
## Обмен
// ── отменил — коллбэк не вызовется ──
merkle_sync_cancel(inst, peer, ns);
Меньший node_id ведёт раунд; второй запрашивает его через WAKE.
WAKE также перезапускает незавершённый раунд после cancel/start второго узла.
// ── разово: завершение ──
merkle_sync_destroy(inst);
```
1. BEGIN(new round) → READY.
2. Ведущий запрашивает хеши детей и обходит различающиеся ветви в глубину.
3. Лист запрашивается страницами с возрастающим курсором.
4. TURN меняет направление: второй узел выполняет такой же pull.
5. Второй проверяет finish и отправляет CHECK(root).
6. Ведущий сверяет корень, проверяет finish и отправляет DONE(root).
7. Второй сверяет DONE со своим текущим корнем.
## 3. Модель данных (коллбэки `merkle_sync_data_ops`)
Ведущий сообщает MT_OK после передачи DONE в надёжный транспорт, если локальная ревизия
не изменилась после проверки. Второй сообщает MT_OK после проверки полученного DONE.
Изменения во время обмена вызывают следующий раунд.
Два завершённых раунда с одной и той же несовпадающей парой корней дают MT_ERR_CONFLICT.
Детерминированное слияние обеспечивает модель; движок не выбирает победителя конфликта.
| Коллбэк | Что делает |
Ожидается только один ответ QUERY. Стек — максимум шесть кадров с сохранёнными соседними ветвями.
Номера запросов проверяются, ответы другого раунда игнорируются.
Повторную передачу обеспечивает ETCP, отдельных таймеров повторов нет.
## Wire
Заголовок: `service:1 | namespace:8 | type:1 | round:8 | request:4` (22 байта).
Все целые big-endian. Максимальный пакет вместе с заголовком — 16384 байта.
Типы 0x10–0x19: WAKE, BEGIN, READY, QUERY, HASHES, PAGE, TURN, CHECK, DONE, ERROR.
| Тип | Payload |
|---|---|
| `update_bucket_hash(ctx, ns, level, prefix64, sha_ctx)` | Перечислить элементы в префиксном диапазоне, вычислить хеш каждого и подать в `sha_ctx` через `EVP_DigestUpdate(sha_ctx, item_hash, 32)`. Возвращает число элементов (0 = бакет пуст, -1 = ошибка) |
| `get_items(ctx, ns, level, prefix, pbytes, buf, len)` | Сериализовать элементы бакета в wire-формат. Возвращает 0 / -2 (буфер мал) / <0 (ошибка) |
| `apply_items(ctx, ns, from_peer, data, len)` | Десериализовать и сохранить полученные элементы. Должна пересчитать дерево через `merkle_sync_recompute_path` для каждого изменённого ключа |
| `apply_update(ctx, ns, key, type, data, len)` | Применить лёгкое обновление (не влияет на Merkle-хеш) — MSG_ITEM_UPDATE |
| WAKE, BEGIN, READY, TURN | пустой; request=0; у WAKE также round=0 |
| QUERY | level:1, prefix:8, after_valid:1, after:8 |
| HASHES | 32 хеша детей по 32 байта в порядке индексов |
| PAGE | more:1, next:8, model_data |
| CHECK, DONE | корень 32 байта; request=0 |
| ERROR | положительный код ошибки 4 байта; request=0 |
`ctx` — прозрачный контекст, передаваемый в `merkle_sync_init` и возвращаемый первым аргументом
в каждый коллбэк. `ns` (namespace) — строка-имя домена данных, обычно `channel_id`.
Курсор продолжения должен возрастать и принадлежать запрошенному листу.
Длины, состояние, тип, префикс и номер запроса проверяются до применения.
Формат model_data определяет адаптер. Старый протокол не поддерживается.
## 4. Формат дерева
## Очереди, диагностика, тесты
5-уровневое префиксное дерево над 64-битными ключами. Каждый уровень берёт 5 старших бит ключа:
уровень 1 — биты 59-63, уровень 5 — биты 39-63. Каждый узел разбивается на 32 бакета (5 бит = 32).
Сессия удерживает ETCP_CONN и исходную очередь. У неё один подготовленный пакет,
один waiter и один отложенный вызов. Передача идёт только после разрешения waiter.
Изменения объединяются флагом dirty; неограниченной out-очереди нет.
Отмена снимает ожидающий и отложенный callback waiter независимо от links_up.
```
level 1 [0..31] корень: 32 бакета по 5 бит
level 2 [0..31]...[0..31] каждый — ещё 32 бакета
...
level 5 [0..31].........[0..31] листья: 32^5 = 33M бакетов макс
```
Категория member_sync: INFO — attach, начало/завершение раунда, detach;
DEBUG — TX/RX, номера запросов, размеры, состояние; ERROR — причина отказа.
Ведущий выводит счётчики переданных и полученных байтов при подтверждении.
- Хеш бакета = `SHA256(хеш_элемента_1 || ... || хеш_элемента_N)`.
- Бакет считается **терминальным** (leaf) на уровне 5 или если в нём `< 8` элементов.
- Хеши бакетов хранятся в таблице `merkle_tree_hash(namespace, level, prefix64, hash, member_count)`.
Вспомогательные функции префикса: `merkle_sync_level_prefix(key, level)` (выделить `level*5`
старших бит) и `merkle_sync_prefix_bytes(level)` (байт на хранение префикса: уровень 1 → 1 байт,
уровень 5 → 4 байта).
## 5. Wire-протокол
Каждое сообщение: `[svc_id:1][ns_len:1][ns:var][type:1][payload:var]`.
| Тип | #define | Назначение |
|---|---|---|
| 0x01 | MSG_HASHES | Сравнение бакетов: bitmap + хеши, либо данные (is_data) |
| 0x02 | MSG_REQUEST | Запрос содержимого различающихся бакетов |
| 0x03 | MSG_BATCH | Передача данных/подхешей запрошенных бакетов |
| 0x04 | MSG_ITEM_UPDATE | Лёгкое обновление (online-статус), вне дерева |
Форматы payload:
- **MSG_HASHES**: `[level:1][prefix_bytes:1][prefix:pb][is_data:1]` + (`[bitmap:4][hash:32]*` | `[dlen:2][data]`)
- **MSG_REQUEST**: `[count:1][ {level:1, prefix_bytes:1, prefix:pb, is_terminal:1} × count ]`
- **MSG_BATCH**: `[count:1][ {level:1, prefix_bytes:1, prefix:pb, is_terminal:1, [data | bitmap+hashes]} × count ]`
- **MSG_ITEM_UPDATE**: `[key:8][type:1][data]`
### Алгоритм обмена
```
A → MSG_HASHES(level=0, bitmap+хеши всех 32 бакетов уровня 1) → B
B сравнивает со своим деревом, находит различающиеся бакеты (differs)
B → MSG_REQUEST(level=2, prefix=X, is_terminal) → A
A → MSG_BATCH(данные бакета или подхеши) → B
B сохраняет, пересчитывает хеши
Рекурсивно для подбакетов, пока хеши не совпадут.
differs==0 и level==0 → корневой хеш совпал → сессия SYNCED.
```
## 6. Сессии и out-очередь (backpressure)
`struct ms_session` — сессия на пару (peer, ns): `sess_state` (`SESS_SYNCING`/`SESS_SYNCED`),
`pending[128]` (ожидающие бакеты: `MS_PEND_WAITING`/`MS_PEND_RESOLVED`, с родительским индексом
и счётчиком подбакетов), `done_cb`/`cb_arg`, `out_q` + `waiter`.
**По-пировая очередь рассылки** `out_q`: рассылка (`broadcast`/`push_update`/`send_to`) не шлёт
немедленно, а кладёт каждый элемент в `out_q` подходящей сессии. Дренаж через `ms_outq_drain_cb`:
`queue_data_get(out_q)` → `etcp_send(conn, entry)`. Перед отправкой проверяется занятость
`conn->send_input_q`; при превышении порога — `queue_waiter_wait(send_input_q, &s->waiter, ...)`
приостанавливает дренаж до освобождения (backpressure, без дропов).
Отправка:
- `_send_msg` — синхронно одному пиру (служебные HASHES/REQUEST/BATCH — по прямому соединению).
- `_broadcast_to_session` — кладёт в `out_q` конкретной сессии (обёртка в MSG_HASHES is_data=1).
## 7. WAL-таймер
Отдельный редкий таймер `MS_WAL_INTERVAL_MS = 60000` (60 с) — `sqlite3_wal_checkpoint_v2(...,
PASSIVE)`. Локальная операция без сети. На Android (`UTUN_HAVE_STANDBY`) при спячке откладывается
через `standby_wait`. Периодической сверки деревьев (bg-таймера) нет — пересчёт событийный.
## 8. API
| Функция | Назначение |
|---|---|
| `merkle_sync_init(inst, svc_id, ops, data_ctx)` | Создать контекст, `_ensure_table`, `etcp_bind(svc_id, _recv_cb)`, WAL-таймер. `inst->msync = ms` |
| `merkle_sync_destroy(inst)` | Отвязать сервис, отменить WAL-таймер, удалить все сессии (коллбэки не вызываются) |
| `merkle_sync_start(inst, peer, ns, done_cb, arg)` | Создать/перезапустить сессию, отправить MSG_HASHES(level=0). Перезапуск теряет старый коллбэк без вызова |
| `merkle_sync_cancel(inst, peer, ns)` | Удалить сессию, `done_cb` не вызывается |
| `merkle_sync_recompute_path(inst, ns, key)` | Пересчитать все 5 уровней для ключа. Возвращает 1 (изменилось) / 0 (no-op) / -1 |
| `merkle_sync_push_update(inst, ns, key, type, data, len)` | MSG_ITEM_UPDATE всем **SYNCED**-сессиям ns |
| `merkle_sync_broadcast(inst, ns, from_peer, data, len)` | Relay: всем сессиям ns, кроме `from_peer` |
| `merkle_sync_send_to(inst, ns, peer, data, len)` | Данные одному пиру (send-back при stale) |
| `merkle_sync_bg_check(inst, ns)` | Проверить один level-5 бакет (сравнение с БД), при расхождении — пересчёт пути вверх. 1/0/-1 |
| `merkle_sync_get_hash(inst, ns, level, prefix64)` | Прочитать хеш бакета (статический буфер, нули если нет) |
| `merkle_sync_level_prefix(key, level)` / `merkle_sync_prefix_bytes(level)` | Арифметика префикса |
## 9. Нюансы
- **svc_id** задаётся при init (member_sync использует `0x31`); единственный приёмник `_recv_cb`.
- **Нет таймаута сессии**: `MT_ERR_TIMEOUT` зарезервирован, но не возникает — сессия живёт до
совпадения корневого хеша или явной отмены.
- **Перезапуск**: `merkle_sync_start` на существующую пару (peer, ns) перезаписывает `done_cb`
(старый коллбэк теряется без вызова).
- **Сессия создаётся и на приёме**: при неожиданном MSG_HASHES от неизвестного пира сессия
создаётся автоматически (symmetrical start).
- **Результат recompute** важен для подавления no-op рассылки: `changed==0` означает, что данные
фактически не изменились.
- Категория логов — `DEBUG_CATEGORY_MEMBER_SYNC`.
test_merkle_sync проверяет индекс, односторонний start, двустороннее слияние, несколько узлов,
параллельные изменения, большой лист с пагинацией и автоматическое обновление.
test_merkle_protocol проверяет ошибочные пакеты/курсоры, сохранение соседних ветвей,
отмену waiter, ошибки apply/SQLite, backpressure, disconnect и повторное подключение.
+122
View File
@@ -0,0 +1,122 @@
#include "merkle_tree.h"
#include "../../lib/debug_config.h"
#include <string.h>
static int tree_error(sqlite3* db, const char* op, const char* ns) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_tree: %s ns=%s: %s", op, ns ? ns : "-", db ? sqlite3_errmsg(db) : "no DB");
return -1;
}
uint64_t merkle_sync_level_prefix(uint64_t key, uint8_t level) {
if (level == 0) return 0;
if (level > MT_MAX_LEVEL) return 0;
return key & (~UINT64_C(0) << (64 - level * 5));
}
uint8_t merkle_sync_prefix_bytes(uint8_t level) {
return level <= MT_MAX_LEVEL ? (uint8_t)((level * 5 + 7) / 8) : 0;
}
int merkle_tree_init(sqlite3* db) {
const char* sql = "DROP TABLE IF EXISTS merkle_tree_hash;"
"CREATE TABLE merkle_tree_hash(namespace TEXT NOT NULL, level INTEGER NOT NULL CHECK(level BETWEEN 0 AND 5),"
"prefix64 INTEGER NOT NULL, hash BLOB NOT NULL CHECK(length(hash)=32),"
"PRIMARY KEY(namespace,level,prefix64))";
if (!db || sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) return tree_error(db, "init", NULL);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "merkle_tree: empty derived index initialized");
return 0;
}
int merkle_tree_get(sqlite3* db, const char* ns, uint8_t level, uint64_t prefix, uint8_t hash[MT_HASH_SIZE]) {
if (!db || !ns || !hash || level > MT_MAX_LEVEL || prefix != merkle_sync_level_prefix(prefix, level))
return tree_error(db, "invalid get", ns);
memset(hash, 0, MT_HASH_SIZE);
sqlite3_stmt* st = NULL;
int rc = sqlite3_prepare_v2(db, "SELECT hash FROM merkle_tree_hash WHERE namespace=? AND level=? AND prefix64=?", -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, ns, -1, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_bind_int(st, 2, level);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 3, (sqlite3_int64)prefix);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
if (rc == SQLITE_ROW && sqlite3_column_bytes(st, 0) == MT_HASH_SIZE) memcpy(hash, sqlite3_column_blob(st, 0), MT_HASH_SIZE);
else if (rc != SQLITE_DONE) { sqlite3_finalize(st); return tree_error(db, "read hash", ns); }
sqlite3_finalize(st);
return 0;
}
int merkle_tree_children(sqlite3* db, const char* ns, uint8_t level, uint64_t prefix,
uint8_t hashes[MT_BUCKETS][MT_HASH_SIZE]) {
if (level >= MT_MAX_LEVEL || prefix != merkle_sync_level_prefix(prefix, level)) return tree_error(db, "invalid children", ns);
if (!db || !ns) return tree_error(db, "invalid children DB", ns);
memset(hashes, 0, MT_BUCKETS * MT_HASH_SIZE);
int shift = 64 - (level + 1) * 5;
const char* sql = level == 0 ? "SELECT prefix64,hash FROM merkle_tree_hash WHERE namespace=? AND level=?"
: "SELECT prefix64,hash FROM merkle_tree_hash WHERE namespace=? AND level=? AND prefix64>=? AND prefix64<=?";
sqlite3_stmt* st = NULL;
int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, ns, -1, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_bind_int(st, 2, level + 1);
if (level && rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 3, (sqlite3_int64)prefix);
if (level && rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 4, (sqlite3_int64)(prefix | (UINT64_C(31) << shift)));
if (rc != SQLITE_OK) { sqlite3_finalize(st); return tree_error(db, "children query", ns); }
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
if (sqlite3_column_bytes(st, 1) != MT_HASH_SIZE) { sqlite3_finalize(st); return tree_error(db, "corrupt child", ns); }
unsigned i = (unsigned)(((uint64_t)sqlite3_column_int64(st, 0) >> shift) & 31);
memcpy(hashes[i], sqlite3_column_blob(st, 1), MT_HASH_SIZE);
}
sqlite3_finalize(st);
if (rc != SQLITE_DONE) return tree_error(db, "children read", ns);
return 0;
}
static int tree_store(sqlite3* db, const char* ns, uint8_t level, uint64_t prefix, const uint8_t hash[MT_HASH_SIZE]) {
static const uint8_t empty[MT_HASH_SIZE];
uint8_t old[MT_HASH_SIZE];
if (merkle_tree_get(db, ns, level, prefix, old) < 0) return -1;
if (!memcmp(old, hash, MT_HASH_SIZE)) return 0;
int is_empty = !memcmp(hash, empty, MT_HASH_SIZE);
const char* sql = is_empty ? "DELETE FROM merkle_tree_hash WHERE namespace=? AND level=? AND prefix64=?"
: "INSERT OR REPLACE INTO merkle_tree_hash(namespace,level,prefix64,hash) VALUES(?,?,?,?)";
sqlite3_stmt* st = NULL;
int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, ns, -1, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_bind_int(st, 2, level);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 3, (sqlite3_int64)prefix);
if (rc == SQLITE_OK && !is_empty) rc = sqlite3_bind_blob(st, 4, hash, MT_HASH_SIZE, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
sqlite3_finalize(st);
if (rc != SQLITE_DONE) return tree_error(db, "write hash", ns);
return 1;
}
int merkle_tree_update(sqlite3* db, const char* ns, uint64_t key, merkle_leaf_hash_fn leaf_hash, void* ctx) {
EVP_MD_CTX* sha = EVP_MD_CTX_new();
uint8_t hash[MT_HASH_SIZE], children[MT_BUCKETS][MT_HASH_SIZE];
static const uint8_t empty[sizeof(children)];
int changed = 0;
if (!db || !ns || !leaf_hash || !sha) goto fail;
for (int level = MT_MAX_LEVEL; level >= 0; level--) {
uint64_t prefix = merkle_sync_level_prefix(key, (uint8_t)level);
uint8_t domain[2] = { 0x4d, (uint8_t)level };
if (EVP_DigestInit_ex(sha, EVP_sha256(), NULL) != 1 || EVP_DigestUpdate(sha, domain, sizeof(domain)) != 1) goto fail;
int count;
if (level == MT_MAX_LEVEL) {
count = leaf_hash(ctx, ns, (uint8_t)level, prefix, sha);
if (count < 0) goto fail;
} else {
if (merkle_tree_children(db, ns, (uint8_t)level, prefix, children) < 0) goto fail;
count = memcmp(children, empty, sizeof(children)) != 0;
if (EVP_DigestUpdate(sha, children, sizeof(children)) != 1) goto fail;
}
if (EVP_DigestFinal_ex(sha, hash, NULL) != 1) goto fail;
if (!count) memset(hash, 0, sizeof(hash));
int rc = tree_store(db, ns, (uint8_t)level, prefix, hash);
if (rc < 0) goto fail;
if (rc == 0) break;
changed = 1;
}
EVP_MD_CTX_free(sha);
return changed;
fail:
EVP_MD_CTX_free(sha);
return tree_error(db, "recompute", ns);
}
+35
View File
@@ -0,0 +1,35 @@
/* merkle_tree — производный индекс хешей в SQLite для сравнения записей модели.
* В каждом namespace дерево SHA256: корень (уровень 0), 5 уровней по 32 ветви.
* Лист выбирается по старшим 25 битам ключа; его содержимое хеширует модель.
* Пустое поддерево имеет нулевой хеш. БД принадлежит вызывающему. */
#ifndef MERKLE_TREE_H
#define MERKLE_TREE_H
#include <stdint.h>
#include <openssl/evp.h>
#include <sqlite3.h>
#define MT_MAX_LEVEL 5
#define MT_BUCKETS 32
#define MT_HASH_SIZE 32
/* Добавить записи листа в уже начатый hash через EVP_DigestUpdate; не завершать hash.
* Возвращает число записей (0 = пустой лист), <0 при ошибке. */
typedef int (*merkle_leaf_hash_fn)(void* ctx, const char* ns, uint8_t level, uint64_t prefix, EVP_MD_CTX* hash);
/* Пересоздать пустой индекс; вызывающий затем восстанавливает его из модели. 0/-1. */
int merkle_tree_init(sqlite3* db);
/* Прочитать хеш уровня 0..5; отсутствующий узел даёт нулевой хеш. 0/-1. */
int merkle_tree_get(sqlite3* db, const char* ns, uint8_t level, uint64_t prefix, uint8_t hash[MT_HASH_SIZE]);
/* Прочитать 32 дочерних хеша узла уровня 0..4; отсутствующие — нули. 0/-1. */
int merkle_tree_children(sqlite3* db, const char* ns, uint8_t level, uint64_t prefix,
uint8_t hashes[MT_BUCKETS][MT_HASH_SIZE]);
/* Пересчитать лист ключа и родителей в транзакции вызывающего.
* 1 = индекс изменился, 0 = прежний хеш, -1 = ошибка (транзакцию нужно откатить). */
int merkle_tree_update(sqlite3* db, const char* ns, uint64_t key, merkle_leaf_hash_fn leaf_hash, void* ctx);
/* Оставить старшие level*5 бит ключа, остальные обнулить; уровень 0 — корень. */
uint64_t merkle_sync_level_prefix(uint64_t key, uint8_t level);
/* Число байтов для старших level*5 бит; 0 для корня или недопустимого уровня. */
uint8_t merkle_sync_prefix_bytes(uint8_t level);
#endif
+14 -3
View File
@@ -833,6 +833,16 @@ static int parse_chatserver(const char *key, const char *value, struct global_co
global->headless_control_bind[sizeof(global->headless_control_bind) - 1] = '\0';
return 0;
}
if (strcmp(key, "call_audio_bind") == 0) {
strncpy(global->call_audio_bind, value, sizeof(global->call_audio_bind) - 1);
global->call_audio_bind[sizeof(global->call_audio_bind) - 1] = '\0';
return 0;
}
if (strcmp(key, "radio_audio_bind") == 0) {
strncpy(global->radio_audio_bind, value, sizeof(global->radio_audio_bind) - 1);
global->radio_audio_bind[sizeof(global->radio_audio_bind) - 1] = '\0';
return 0;
}
return -1;
}
@@ -908,12 +918,13 @@ static int parse_reality(const char *key, const char *value, struct reality_conf
return 0;
}
if (strcmp(key, "time_window") == 0) { rc->time_window_sec = atoi(value); return 0; }
if (strcmp(key, "relay_idle_timeout") == 0) { rc->relay_idle_timeout_sec = atoi(value); return 0; }
if (strcmp(key, "fingerprint") == 0) {
if (strcmp(value, "chrome") == 0) { rc->fingerprint = REALITY_FP_CHROME; return 0; }
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: unknown reality fingerprint '%s' (valid: chrome)", filename, line_num, value);
return -1;
}
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown reality option '%s'. Valid: enabled, server_name, dest, short_id, short_ids, public_key, private_key, version, time_window, fingerprint", filename, line_num, key);
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown reality option '%s'. Valid: enabled, server_name, dest, short_id, short_ids, public_key, private_key, version, time_window, relay_idle_timeout, fingerprint", filename, line_num, key);
return -1;
}
@@ -1232,7 +1243,7 @@ static struct utun_config* parse_config_internal(FILE *fp, const char *filename)
assign_string(cfg->global.db_path, sizeof(cfg->global.db_path), value);
} else if (parse_chatserver(key, value, &cfg->global, filename, line_num) < 0 &&
chat_setting_set_state(&cfg->global.chat_settings, key, value) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown chatserver option '%s'. Valid: db_path, join_password, allowed_groups, storage_total_size, storage_unit_size, storage_autoload, opus_codec_preset, compressor_enabled, compressor_max_gain_db, compressor_rise_rate, media_download_max_peers", filename, line_num, key);
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown chatserver option '%s'. Valid: db_path, join_password, allowed_groups, storage_total_size, storage_unit_size, storage_autoload, opus_codec_preset, radio_opus_mode, radio_opus_bitrate, radio_capture_stereo, compressor_enabled, compressor_max_gain_db, compressor_rise_rate, radio_compressor_enabled, radio_compressor_max_gain_db, media_download_max_peers", filename, line_num, key);
}
break;
case SECTION_GUI:
@@ -1391,7 +1402,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
break;
case SECTION_NETWORK: if (cur_network) parse_network(key, value, cur_network, filename, line_num); break;
case SECTION_NTP: if (strcmp(key, "enabled") == 0) cfg->global.ntp_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; else if (strcmp(key, "server") == 0) { struct CFG_NTP_SERVER *ns = u_calloc(1, sizeof(struct CFG_NTP_SERVER)); if (ns) { strncpy(ns->name, value, sizeof(ns->name) - 1); ns->name[sizeof(ns->name) - 1] = '\0'; ns->next = cfg->global.ntp_servers; cfg->global.ntp_servers = ns; cfg->global.ntp_server_count++; } } else if (strcmp(key, "interval") == 0) { cfg->global.ntp_resync_interval = atoi(value); if (cfg->global.ntp_resync_interval < 60) cfg->global.ntp_resync_interval = 60; } else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown ntp option '%s'", filename, line_num, key); break;
case SECTION_CHATSERVER: cfg->global.chatserver_enabled = 1; cfg->global.db_sync_enabled = 1; if (strcmp(key, "db_path") == 0) assign_string(cfg->global.db_path, sizeof(cfg->global.db_path), value); else if (parse_chatserver(key, value, &cfg->global, filename, line_num) < 0 && chat_setting_set_state(&cfg->global.chat_settings, key, value) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown chatserver option '%s'. Valid: db_path, join_password, allowed_groups, storage_total_size, storage_unit_size, storage_autoload, opus_codec_preset, compressor_enabled, compressor_max_gain_db, compressor_rise_rate, media_download_max_peers", filename, line_num, key); } break;
case SECTION_CHATSERVER: cfg->global.chatserver_enabled = 1; cfg->global.db_sync_enabled = 1; if (strcmp(key, "db_path") == 0) assign_string(cfg->global.db_path, sizeof(cfg->global.db_path), value); else if (parse_chatserver(key, value, &cfg->global, filename, line_num) < 0 && chat_setting_set_state(&cfg->global.chat_settings, key, value) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown chatserver option '%s'. Valid: db_path, join_password, allowed_groups, storage_total_size, storage_unit_size, storage_autoload, opus_codec_preset, radio_opus_mode, radio_opus_bitrate, radio_capture_stereo, compressor_enabled, compressor_max_gain_db, compressor_rise_rate, radio_compressor_enabled, radio_compressor_max_gain_db, media_download_max_peers", filename, line_num, key); } break;
case SECTION_GUI: break;
case SECTION_REALITY: parse_reality(key, value, &cfg->global.reality, filename, line_num); break;
case SECTION_SOCKS: parse_socks(key, value, &cfg->global, filename, line_num); break;
+2
View File
@@ -222,6 +222,8 @@ struct global_config {
uint64_t chatserver_storage_total_size; // bytes, общий лимит хранилища (0 = безлимит)
uint64_t chatserver_storage_unit_size; // bytes, лимит одного файла (0 = отключено)
char headless_control_bind[64]; // ip:port для headless chat CLI (пусто = отключено)
char call_audio_bind[64]; // ip:port для headless аудио-сокета звонков (пусто = отключено)
char radio_audio_bind[64]; // ip:port для headless аудио-сокета рации (пусто = отключено)
struct chat_setting_state chat_settings; // chat-настройки из [chatserver] (per-instance)
// Reality-камуфляж ([reality] section)
+1 -1
View File
@@ -479,7 +479,7 @@ int config_ensure_keys_and_node_id(const char *filename) {
buf_capacity = file_size + 1024;
work_buf = u_malloc(buf_capacity);
if (!work_buf) { u_free(file_buf); return -1; }
memcpy(work_buf, file_buf, file_size);
memcpy(work_buf, file_buf, file_size + 1); /* Helpers use strchr; preserve the terminating NUL. */
work_len = file_size;
u_free(file_buf); file_buf = NULL;
}
+3 -1
View File
@@ -217,6 +217,7 @@ int control_server_init(struct control_server* server,
accept_callback,
NULL, /* write callback */
NULL, /* except callback */
"control_listen",
server);
if (!server->listen_socket_id) {
@@ -434,6 +435,7 @@ static void accept_callback(socket_t fd, void* arg) {
client_read_callback,
client_write_callback,
client_except_callback,
"control_client",
client);
if (!client->socket_id) {
@@ -1209,7 +1211,7 @@ static void send_metrics(struct control_server* server, struct control_client* c
struct ll_entry* re = instance->router_conns->head;
while (re) {
struct ETCP_ROUTER_CONN* rc = (struct ETCP_ROUTER_CONN*)re;
rsp->router.total_inflight += (uint32_t)(rc->tx_seq - rc->tx_acked);
rsp->router.total_inflight += (uint32_t)queue_entry_count(rc->inflight_q);
if (rc->send_q) rsp->router.total_send_q += (uint32_t)queue_entry_count(rc->send_q);
if (rc->recv_q) rsp->router.total_recv_q += (uint32_t)queue_entry_count(rc->recv_q);
rsp->router.pkts_sent += rc->c_pkts_sent;
+372 -58
View File
@@ -109,8 +109,9 @@ static int dm_node_pubkeys(struct dm_state* dm, uint64_t node_id, uint8_t x25519
return rc;
}
/* Есть ли общая группа с пиром (anti-spam: принимаем DM только от знакомых из общих каналов). */
static int dm_share_group(struct dm_state* dm, uint64_t peer_id) {
/* Общий CHAT-канал с пиром. Возвращает 1 и кладёт group_id (== channel_id) в *out_group_id. */
static int dm_shared_group_id(struct dm_state* dm, uint64_t peer_id, uint64_t* out_group_id) {
if (!dm || !out_group_id) return 0;
uint64_t* ch_ids = NULL;
int ch_count = 0;
if (topo_node_sqlite_get_member_channels(dm->db, dm->inst->node_id, &ch_ids, &ch_count) != 0)
@@ -118,12 +119,49 @@ static int dm_share_group(struct dm_state* dm, uint64_t peer_id) {
int shared = 0;
for (int i = 0; i < ch_count; i++) {
char ch[64]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)ch_ids[i]);
if (topo_node_sqlite_member_in_channel(dm->db, ch, peer_id)) { shared = 1; break; }
if (topo_node_sqlite_member_in_channel(dm->db, ch, peer_id)) {
*out_group_id = ch_ids[i];
shared = 1;
break;
}
}
if (ch_ids) u_free(ch_ids);
return shared;
}
/* Есть ли общая группа с пиром (anti-spam: принимаем DM только от знакомых из общих каналов). */
static int dm_share_group(struct dm_state* dm, uint64_t peer_id) {
uint64_t gid = 0;
return dm_shared_group_id(dm, peer_id, &gid);
}
/* Собрать node_id всех мемберов общих групп (без self и дублей). До max штук.
* Используется для offline-pull: потенциальные отправители почты = мемберы общих групп. */
static int dm_shared_peers(struct dm_state* dm, uint64_t* out, int max) {
if (!dm || !out || max <= 0) return 0;
uint64_t* ch_ids = NULL;
int ch_count = 0;
if (topo_node_sqlite_get_member_channels(dm->db, dm->inst->node_id, &ch_ids, &ch_count) != 0)
return 0;
int found = 0;
for (int c = 0; c < ch_count && found < max; c++) {
char tbl[80]; snprintf(tbl, sizeof(tbl), "peers_%llu", (unsigned long long)ch_ids[c]);
char sql[256]; snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\"", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db, sql, -1, &st, NULL) != SQLITE_OK) continue;
while (sqlite3_step(st) == SQLITE_ROW && found < max) {
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
if (nid == dm->inst->node_id) continue;
int dup = 0;
for (int i = 0; i < found; i++) if (out[i] == nid) { dup = 1; break; }
if (!dup) out[found++] = nid;
}
sqlite3_finalize(st);
}
if (ch_ids) u_free(ch_ids);
return found;
}
/* Доступен ли пир для прямой доставки: живой ETCP-линк или присутствие в BGP-группе. */
static int dm_peer_reachable(struct dm_state* dm, uint64_t peer_id) {
struct ETCP_CONN* conn = instance_find_conn(dm->inst, peer_id);
@@ -191,6 +229,12 @@ static int dm_conv_load(struct dm_state* dm, const char* conv_id, struct dm_conv
c->group_id = (uint64_t)sqlite3_column_int64(st, 4);
c->last_out_seq = (uint64_t)sqlite3_column_int64(st, 5);
c->last_in_seq = (uint64_t)sqlite3_column_int64(st, 6);
/* Старые беседы могли быть авто-созданы с group_id=0 (маршрутизация только напрямую) —
* подтягиваем общую группу, чтобы DM шёл через неё (indirect/BGP). */
if (c->group_id == 0) {
uint64_t gid = 0;
if (dm_shared_group_id(dm, c->peer_node_id, &gid)) c->group_id = gid;
}
dm_derive_content_key(dm->inst->my_keys.private_key, c->peer_x25519, c->content_key);
rc = 0;
}
@@ -299,22 +343,33 @@ static void dm_on_msg(struct dm_state* dm, const uint8_t* msg, size_t mlen) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: conv_id mismatch from 0x%016llx", DM_ID, (unsigned long long)author);
return;
}
if (!dm_share_group(dm, author)) {
uint64_t gid = 0;
if (!dm_shared_group_id(dm, author, &gid)) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: no shared group with 0x%016llx — reject DM", DM_ID, (unsigned long long)author);
return;
}
memset(&c, 0, sizeof(c));
snprintf(c.conv_id, sizeof(c.conv_id), "%s", conv_id);
c.peer_node_id = author;
c.group_id = gid;
if (dm_node_pubkeys(dm, author, c.peer_x25519, c.peer_ed25519) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: no pubkeys for 0x%016llx", DM_ID, (unsigned long long)author);
return;
}
chat_core_get_node_name(dm->inst, author, c.peer_name, sizeof(c.peer_name));
dm_derive_content_key(dm->inst->my_keys.private_key, c.peer_x25519, c.content_key);
c.group_id = 0;
dm_conv_save(dm, &c);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: auto-created conversation conv=%s peer=0x%016llx",
DM_ID, conv_id, (unsigned long long)author);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: auto-created conversation conv=%s peer=0x%016llx name=%s group=%llu",
DM_ID, conv_id, (unsigned long long)author, c.peer_name, (unsigned long long)c.group_id);
/* событие GUI: появилась новая беседа (чтобы GUI добавил её в список) */
{
size_t cl = strlen(conv_id);
uint8_t evt[1 + 64];
evt[0] = (uint8_t)cl;
memcpy(evt + 1, conv_id, cl);
chat_event_post(dm->inst, CHAT_EVT_DM_CONV_UPDATED, evt, 1 + (int)cl);
}
}
/* проверка подписи автора по всем полям до sig */
@@ -415,7 +470,54 @@ static void dm_on_hello(struct dm_state* dm, uint64_t conv_num, uint64_t from_pe
snprintf(conv_id, sizeof(conv_id), "%llu", (unsigned long long)conv_num);
struct dm_conv c;
if (dm_conv_load(dm, conv_id, &c) != 0) return;
if (dm_conv_load(dm, conv_id, &c) != 0) {
/* Беседы нет локально (первое сообщение ушло, пока мы были offline):
* автосоздать её (anti-spam: conv_id + общая группа) и ответить HELLO,
* чтобы отправитель дослал пропущенные сообщения. */
if (dm_derive_conv_id(dm->inst->node_id, from_peer) != conv_num) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: hello conv_id mismatch from 0x%016llx", DM_ID, (unsigned long long)from_peer);
return;
}
uint64_t gid = 0;
if (!dm_shared_group_id(dm, from_peer, &gid)) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: hello no shared group with 0x%016llx — ignore", DM_ID, (unsigned long long)from_peer);
return;
}
memset(&c, 0, sizeof(c));
snprintf(c.conv_id, sizeof(c.conv_id), "%s", conv_id);
c.peer_node_id = from_peer;
c.group_id = gid;
if (dm_node_pubkeys(dm, from_peer, c.peer_x25519, c.peer_ed25519) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: hello no pubkeys for 0x%016llx", DM_ID, (unsigned long long)from_peer);
return;
}
chat_core_get_node_name(dm->inst, from_peer, c.peer_name, sizeof(c.peer_name));
dm_derive_content_key(dm->inst->my_keys.private_key, c.peer_x25519, c.content_key);
dm_conv_save(dm, &c);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: hello auto-created conversation conv=%s peer=0x%016llx name=%s group=%llu",
DM_ID, conv_id, (unsigned long long)from_peer, c.peer_name, (unsigned long long)c.group_id);
{
size_t cl = strlen(conv_id);
uint8_t evt[1 + 64];
evt[0] = (uint8_t)cl;
memcpy(evt + 1, conv_id, cl);
chat_event_post(dm->inst, CHAT_EVT_DM_CONV_UPDATED, evt, 1 + (int)cl);
}
/* ответить HELLO (out=0,in=0) — отправитель увидит peer_in_seq < last_out_seq и дослал сообщения */
uint8_t hello[24];
uint64_t z = 0;
memcpy(hello, &conv_num, 8);
memcpy(hello + 8, &z, 8);
memcpy(hello + 16, &z, 8);
if (dm_route_send(dm, 0, from_peer, DM_SUBCMD_HELLO, hello, sizeof(hello)) != 0)
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: hello reply send failed peer=0x%016llx", DM_ID, (unsigned long long)from_peer);
else
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: hello reply → 0x%016llx conv=%s (auto-created)",
DM_ID, (unsigned long long)from_peer, conv_id);
return;
}
if (peer_in_seq < c.last_out_seq) {
sqlite3_stmt* st = NULL;
@@ -506,33 +608,60 @@ static void dm_on_conn_status(struct ETCP_CONN* conn, int status, void* arg) {
/* На подъём ЛЮБОГО соединения: HELLO для беседы с подключившимся пиром +
* PULL mailbox для бесед, чей пир не подключён напрямую (offline-доставка). */
uint64_t conv_peers[64];
int nconv = 0;
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"SELECT conv_id, peer_node_id, last_out_seq, last_in_seq FROM dm_conversations",
-1, &st, NULL) != SQLITE_OK) return;
while (sqlite3_step(st) == SQLITE_ROW) {
const char* conv_id = (const char*)sqlite3_column_text(st, 0);
uint64_t conv_peer = (uint64_t)sqlite3_column_int64(st, 1);
uint64_t out_seq = (uint64_t)sqlite3_column_int64(st, 2);
uint64_t in_seq = (uint64_t)sqlite3_column_int64(st, 3);
uint64_t conv_num = strtoull(conv_id, NULL, 10);
-1, &st, NULL) == SQLITE_OK) {
while (sqlite3_step(st) == SQLITE_ROW) {
const char* conv_id = (const char*)sqlite3_column_text(st, 0);
uint64_t conv_peer = (uint64_t)sqlite3_column_int64(st, 1);
uint64_t out_seq = (uint64_t)sqlite3_column_int64(st, 2);
uint64_t in_seq = (uint64_t)sqlite3_column_int64(st, 3);
uint64_t conv_num = strtoull(conv_id, NULL, 10);
if (conv_peer == peer) {
uint8_t hello[24];
memcpy(hello, &conv_num, 8);
memcpy(hello + 8, &out_seq, 8);
memcpy(hello + 16, &in_seq, 8);
/* group_id берём из строки беседы — для простоты HELLO по 0, маршрутизация глобальная */
dm_route_send(dm, 0, peer, DM_SUBCMD_HELLO, hello, sizeof(hello));
}
if (nconv < (int)(sizeof(conv_peers) / sizeof(conv_peers[0])))
conv_peers[nconv++] = conv_peer;
/* пир не подключён напрямую — тянем mailbox со storage-узлов */
struct ETCP_CONN* pc = instance_find_conn(inst, conv_peer);
if (!pc || !pc->links_up || !pc->initialized) {
dm_mailbox_pull(inst, conv_peer, in_seq);
if (conv_peer == peer) {
uint8_t hello[24];
memcpy(hello, &conv_num, 8);
memcpy(hello + 8, &out_seq, 8);
memcpy(hello + 16, &in_seq, 8);
/* group_id берём из строки беседы — для простоты HELLO по 0, маршрутизация глобальная */
if (dm_route_send(dm, 0, peer, DM_SUBCMD_HELLO, hello, sizeof(hello)) != 0)
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: hello send failed conv=%s peer=0x%016llx",
DM_ID, conv_id, (unsigned long long)peer);
else
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: hello → 0x%016llx conv=%s out=%llu in=%llu",
DM_ID, (unsigned long long)peer, conv_id,
(unsigned long long)out_seq, (unsigned long long)in_seq);
}
/* пир не подключён напрямую — тянем mailbox со storage-узлов */
struct ETCP_CONN* pc = instance_find_conn(inst, conv_peer);
if (!pc || !pc->links_up || !pc->initialized) {
dm_mailbox_pull(inst, conv_peer, in_seq);
}
}
sqlite3_finalize(st);
}
/* Первое сообщение могло прийти, пока мы были offline: мемберы общих групп,
* с которыми ещё нет беседы, тоже опрашиваем mailbox (отправитель мог уйти
* offline, тогда HELLO-восстановление недоступно). */
uint64_t peers[128];
int np = dm_shared_peers(dm, peers, (int)(sizeof(peers) / sizeof(peers[0])));
for (int i = 0; i < np; i++) {
uint64_t p = peers[i];
struct ETCP_CONN* pc = instance_find_conn(inst, p);
if (pc && pc->links_up && pc->initialized) continue; /* online — HELLO-путь */
int has_conv = 0;
for (int j = 0; j < nconv; j++) if (conv_peers[j] == p) { has_conv = 1; break; }
if (has_conv) continue; /* уже обработан циклом выше */
dm_mailbox_pull(inst, p, 0);
}
sqlite3_finalize(st);
}
/* ── публичный API ── */
@@ -553,28 +682,39 @@ int dm_start(struct UTUN_INSTANCE* inst, uint64_t peer_node_id,
struct dm_conv c;
if (dm_conv_load(dm, conv_id, &c) == 0) {
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: conversation already exists conv=%s", DM_ID, conv_id);
return 0;
/* Беседа уже есть, но могла быть авто-создана с group_id=0 — дописываем из source_ch_id,
* чтобы маршрутизация шла через группу (а не только прямое соединение). */
if (c.group_id == 0 && source_ch_id && source_ch_id[0]) {
uint64_t gid = strtoull(source_ch_id, NULL, 10);
if (gid != 0) {
c.group_id = gid;
dm_conv_save(dm, &c);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: conversation exists conv=%s — updated group=%llu",
DM_ID, conv_id, (unsigned long long)gid);
}
} else {
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: conversation already exists conv=%s", DM_ID, conv_id);
}
} else {
memset(&c, 0, sizeof(c));
snprintf(c.conv_id, sizeof(c.conv_id), "%s", conv_id);
c.peer_node_id = peer_node_id;
memcpy(c.peer_x25519, peer_x25519, 32);
memcpy(c.peer_ed25519, peer_ed25519, 32);
if (peer_name) snprintf(c.peer_name, sizeof(c.peer_name), "%s", peer_name);
c.group_id = source_ch_id ? strtoull(source_ch_id, NULL, 10) : 0;
dm_derive_content_key(inst->my_keys.private_key, c.peer_x25519, c.content_key);
if (dm_conv_save(dm, &c) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: conv_save failed conv=%s", DM_ID, conv_id);
return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: started conv=%s peer=0x%016llx name=%s group=%llu",
DM_ID, conv_id, (unsigned long long)peer_node_id, c.peer_name,
(unsigned long long)c.group_id);
}
memset(&c, 0, sizeof(c));
snprintf(c.conv_id, sizeof(c.conv_id), "%s", conv_id);
c.peer_node_id = peer_node_id;
memcpy(c.peer_x25519, peer_x25519, 32);
memcpy(c.peer_ed25519, peer_ed25519, 32);
if (peer_name) snprintf(c.peer_name, sizeof(c.peer_name), "%s", peer_name);
c.group_id = source_ch_id ? strtoull(source_ch_id, NULL, 10) : 0;
dm_derive_content_key(inst->my_keys.private_key, c.peer_x25519, c.content_key);
if (dm_conv_save(dm, &c) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: conv_save failed conv=%s", DM_ID, conv_id);
return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: started conv=%s peer=0x%016llx name=%s group=%llu",
DM_ID, conv_id, (unsigned long long)peer_node_id, c.peer_name,
(unsigned long long)c.group_id);
/* событие GUI */
/* событие GUI (всегда, чтобы GUI мог открыть/выделить беседу) */
{
size_t cl = strlen(conv_id);
uint8_t evt[1 + 64];
@@ -634,15 +774,32 @@ int dm_send(struct UTUN_INSTANCE* inst, const char* conv_id, const char* content
/* доставка: доступен → прямой send, иначе → mailbox */
if (dm_peer_reachable(dm, c.peer_node_id)) {
dm_route_send(dm, c.group_id, c.peer_node_id, DM_SUBCMD_MSG, body, body_len);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: sent direct conv=%s seq=%llu → 0x%016llx",
DM_ID, conv_id, (unsigned long long)seq, (unsigned long long)c.peer_node_id);
if (dm_route_send(dm, c.group_id, c.peer_node_id, DM_SUBCMD_MSG, body, body_len) != 0)
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: direct send failed conv=%s seq=%llu → 0x%016llx",
DM_ID, conv_id, (unsigned long long)seq, (unsigned long long)c.peer_node_id);
else
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: sent direct conv=%s seq=%llu → 0x%016llx",
DM_ID, conv_id, (unsigned long long)seq, (unsigned long long)c.peer_node_id);
} else {
dm_mailbox_put(inst, c.peer_node_id, inst->node_id, body, body_len);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: peer offline conv=%s seq=%llu → mailbox",
DM_ID, conv_id, (unsigned long long)seq);
if (dm_mailbox_put(inst, c.peer_node_id, inst->node_id, body, body_len) != 0)
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: mailbox put failed conv=%s seq=%llu (peer offline, no storage)",
DM_ID, conv_id, (unsigned long long)seq);
else
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: peer offline conv=%s seq=%llu → mailbox",
DM_ID, conv_id, (unsigned long long)seq);
}
u_free(body);
/* событие GUI (своё сообщение тоже, чтобы GUI обновил список) */
{
size_t cl = strlen(conv_id);
uint8_t evt[1 + 64 + 8];
evt[0] = (uint8_t)cl;
memcpy(evt + 1, conv_id, cl);
uint64_t me = inst->node_id;
memcpy(evt + 1 + cl, &me, 8);
chat_event_post(inst, CHAT_EVT_DM_MSG_RECEIVED, evt, 1 + (int)cl + 8);
}
return 0;
}
@@ -689,6 +846,26 @@ void dm_core_destroy(struct UTUN_INSTANCE* inst) {
/* ── JSON для GUI/headless ── */
/* Экранирование строки для вставки в JSON-строку: " \ \n \r \t, прочие <0x20 — drop. */
static void json_escape(const char* src, char* dst, size_t dst_sz) {
size_t i = 0;
if (!src) src = "";
for (; *src && i + 2 < dst_sz; src++) {
unsigned char c = (unsigned char)*src;
switch (c) {
case '"': dst[i++] = '\\'; dst[i++] = '"'; break;
case '\\': dst[i++] = '\\'; dst[i++] = '\\'; break;
case '\n': dst[i++] = '\\'; dst[i++] = 'n'; break;
case '\r': dst[i++] = '\\'; dst[i++] = 'r'; break;
case '\t': dst[i++] = '\\'; dst[i++] = 't'; break;
default:
if (c < 0x20) break;
dst[i++] = (char)c;
}
}
dst[i] = '\0';
}
/* Список бесед в JSON. */
int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len) {
struct dm_state* dm = dm_of(inst);
@@ -708,11 +885,19 @@ int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf
uint64_t last_ts = (uint64_t)sqlite3_column_int64(st, 3);
uint64_t out_seq = (uint64_t)sqlite3_column_int64(st, 4);
uint64_t in_seq = (uint64_t)sqlite3_column_int64(st, 5);
char resolved[128];
if (!name || !name[0]) {
resolved[0] = '\0';
chat_core_get_node_name(inst, peer, resolved, sizeof(resolved));
name = resolved;
}
char esc_name[512];
json_escape(name ? name : "", esc_name, sizeof(esc_name));
off += snprintf(buf + off, buf_size - (size_t)off,
"%s{\"conv_id\":\"%s\",\"peer_node_id\":%llu,\"peer_name\":\"%s\","
"\"last_ts\":%llu,\"out_seq\":%llu,\"in_seq\":%llu}",
first ? "" : ",", conv_id, (unsigned long long)peer,
name ? name : "", (unsigned long long)last_ts,
esc_name, (unsigned long long)last_ts,
(unsigned long long)out_seq, (unsigned long long)in_seq);
first = 0;
}
@@ -735,11 +920,11 @@ int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int c
char sql[256];
if (count > 0)
snprintf(sql, sizeof(sql),
"SELECT dir,seq,ts,author,ct,data FROM dm_messages WHERE conv_id=? ORDER BY seq LIMIT %d OFFSET %d",
"SELECT dir,seq,ts,author,ct,data FROM dm_messages WHERE conv_id=? ORDER BY ts, seq LIMIT %d OFFSET %d",
count, offset);
else
snprintf(sql, sizeof(sql),
"SELECT dir,seq,ts,author,ct,data FROM dm_messages WHERE conv_id=? ORDER BY seq");
"SELECT dir,seq,ts,author,ct,data FROM dm_messages WHERE conv_id=? ORDER BY ts, seq");
int off = 0;
off += snprintf(buf + off, buf_size - (size_t)off, "[");
@@ -771,10 +956,13 @@ int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int c
}
u_free(plain);
}
char esc_ct[64], esc_data[DM_DATA_MAX * 2 + 8];
json_escape(ct ? ct : "", esc_ct, sizeof(esc_ct));
json_escape(text, esc_data, sizeof(esc_data));
off += snprintf(buf + off, buf_size - (size_t)off,
"%s{\"dir\":%d,\"seq\":%llu,\"ts\":%llu,\"author\":%llu,\"ct\":\"%s\",\"data\":\"%s\"}",
first ? "" : ",", dir, (unsigned long long)seq, (unsigned long long)ts,
(unsigned long long)author, ct ? ct : "", text);
(unsigned long long)author, esc_ct, esc_data);
first = 0;
}
}
@@ -783,3 +971,129 @@ int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int c
*out_len = (size_t)off;
return 0;
}
/* ── трамплины для gui_bridge (GUI → uasync) ── */
/* Начать беседу с узлом из общей группы: находим pubkey пира в nodes, зовём dm_start. */
void dm_start_trampoline(void* arg) {
struct dm_start_req* req = (struct dm_start_req*)arg;
if (!req) return;
struct UTUN_INSTANCE* inst = req->inst;
uint64_t peer = req->peer_node_id;
char src[64]; strncpy(src, req->source_ch_id, sizeof(src) - 1); src[sizeof(src) - 1] = '\0';
char name[128]; strncpy(name, req->peer_name, sizeof(name) - 1); name[sizeof(name) - 1] = '\0';
u_free(arg);
struct dm_state* dm = dm_of(inst);
if (!dm || !dm->initialized || !inst || !peer) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: dm_start_trampoline — not initialized", DM_ID);
return;
}
if (peer == inst->node_id) return;
uint8_t x25519[32], ed25519[32];
if (dm_node_pubkeys(dm, peer, x25519, ed25519) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: dm_start_trampoline — no pubkeys for peer=0x%016llx",
DM_ID, (unsigned long long)peer);
return;
}
if (dm_start(inst, peer, x25519, ed25519, name[0] ? name : NULL, src[0] ? src : NULL) != 0)
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: dm_start_trampoline — dm_start failed peer=0x%016llx",
DM_ID, (unsigned long long)peer);
}
/* Отправить текстовое сообщение в беседу. */
void dm_send_trampoline(void* arg) {
struct dm_send_req* req = (struct dm_send_req*)arg;
if (!req) return;
struct UTUN_INSTANCE* inst = req->inst;
char conv[64]; strncpy(conv, req->conv_id, sizeof(conv) - 1); conv[sizeof(conv) - 1] = '\0';
int rc = dm_send(inst, conv, "text", req->data, req->data_len);
u_free(arg);
if (rc != 0)
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: dm_send_trampoline — dm_send failed conv=%s",
DM_ID, conv);
}
/* Собрать расшифрованные сообщения беседы и отправить в GUI бинарным событием:
* [conv_id_len:1][conv_id:var][count:2]([dir:1][seq:8][ts:8][author:8][ct_len:1][ct:var][data_len:2][data:var])* */
void dm_messages_trampoline(void* arg) {
struct dm_messages_req* req = (struct dm_messages_req*)arg;
if (!req) return;
struct UTUN_INSTANCE* inst = req->inst;
char conv_id[64]; strncpy(conv_id, req->conv_id, sizeof(conv_id) - 1); conv_id[sizeof(conv_id) - 1] = '\0';
u_free(arg);
struct dm_state* dm = dm_of(inst);
if (!dm || !dm->initialized) return;
struct dm_conv c;
if (dm_conv_load(dm, conv_id, &c) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: dm_messages_trampoline — no conversation %s", DM_ID, conv_id);
return;
}
uint64_t conv_num = (uint64_t)strtoull(conv_id, NULL, 10);
sqlite3_stmt* st = NULL;
uint8_t* msgs = NULL;
size_t msgs_len = 0, msgs_cap = 0;
int count = 0;
if (sqlite3_prepare_v2(dm->db,
"SELECT dir,seq,ts,author,ct,data FROM dm_messages WHERE conv_id=? ORDER BY ts, seq",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC);
while (sqlite3_step(st) == SQLITE_ROW) {
uint8_t dir = (uint8_t)sqlite3_column_int(st, 0);
uint64_t seq = (uint64_t)sqlite3_column_int64(st, 1);
uint64_t ts = (uint64_t)sqlite3_column_int64(st, 2);
uint64_t author = (uint64_t)sqlite3_column_int64(st, 3);
const char* ct = (const char*)sqlite3_column_text(st, 4);
const uint8_t* enc = (const uint8_t*)sqlite3_column_blob(st, 5);
int enc_len = sqlite3_column_bytes(st, 5);
size_t ct_len = ct ? strnlen(ct, 255) : 0;
uint8_t plain[DM_DATA_MAX];
size_t plen = 0;
if (enc && enc_len >= (int)DM_TAG_SIZE) {
uint8_t nonce[DM_NONCE_SIZE];
dm_build_nonce(conv_num, author, seq, nonce);
if (dm_decrypt(c.content_key, nonce, enc, (size_t)enc_len, plain, &plen) != 0)
plen = 0;
}
size_t need = 1 + 8 + 8 + 8 + 1 + ct_len + 2 + plen;
if (msgs_len + need > msgs_cap) {
msgs_cap = msgs_cap ? msgs_cap * 2 : 4096;
while (msgs_cap < msgs_len + need) msgs_cap *= 2;
uint8_t* tmp = u_realloc(msgs, msgs_cap);
if (!tmp) break;
msgs = tmp;
}
uint16_t plen16 = (uint16_t)plen;
msgs[msgs_len] = dir; msgs_len += 1;
memcpy(msgs + msgs_len, &seq, 8); msgs_len += 8;
memcpy(msgs + msgs_len, &ts, 8); msgs_len += 8;
memcpy(msgs + msgs_len, &author, 8); msgs_len += 8;
msgs[msgs_len] = (uint8_t)ct_len; msgs_len += 1;
if (ct_len) { memcpy(msgs + msgs_len, ct, ct_len); msgs_len += ct_len; }
memcpy(msgs + msgs_len, &plen16, 2); msgs_len += 2;
if (plen) { memcpy(msgs + msgs_len, plain, plen); msgs_len += plen; }
count++;
}
sqlite3_finalize(st);
}
size_t cl = strlen(conv_id);
size_t evt_sz = 1 + cl + 2 + msgs_len;
uint8_t* evt = u_malloc(evt_sz);
if (!evt) { if (msgs) u_free(msgs); return; }
uint16_t u16cnt = (uint16_t)count;
evt[0] = (uint8_t)cl;
memcpy(evt + 1, conv_id, cl);
memcpy(evt + 1 + cl, &u16cnt, 2);
if (msgs_len) memcpy(evt + 1 + cl + 2, msgs, msgs_len);
chat_event_post(inst, CHAT_EVT_DM_MESSAGES, evt, (int)evt_sz);
u_free(evt);
if (msgs) u_free(msgs);
}
+30
View File
@@ -70,6 +70,36 @@ int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf
int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int count, int offset,
char* buf, size_t buf_size, size_t* out_len);
/* ── Трамплины для gui_bridge (GUI → uasync) ── */
/* Начать беседу с узлом из общей группы. Трамплин сам находит x25519/ed25519
* пира в таблице nodes (как headless hc_handle_dm_start), после чего вызывает
* dm_start(). Результат — событие CHAT_EVT_DM_CONV_UPDATED. */
struct dm_start_req {
struct UTUN_INSTANCE* inst;
uint64_t peer_node_id;
char source_ch_id[64];
char peer_name[128];
};
void dm_start_trampoline(void* arg);
/* Отправить текстовое сообщение в беседу (content_type="text"). */
struct dm_send_req {
struct UTUN_INSTANCE* inst;
char conv_id[64];
uint32_t data_len;
uint8_t* data; /* data_len байт (выделяется в хвосте структуры) */
};
void dm_send_trampoline(void* arg);
/* Запросить расшифрованные сообщения беседы. Результат — CHAT_EVT_DM_MESSAGES:
* [conv_id_len:1][conv_id:var][count:2]([dir:1][seq:8][ts:8][author:8][ct_len:1][ct:var][data_len:2][data:var])* */
struct dm_messages_req {
struct UTUN_INSTANCE* inst;
char conv_id[64];
};
void dm_messages_trampoline(void* arg);
#ifdef __cplusplus
}
#endif
+2 -2
View File
@@ -269,7 +269,7 @@ void dm_mailbox_destroy(struct UTUN_INSTANCE* inst) {
/* ── Публичные операции ── */
/* Собрать storage-узлы (dm_storage=yes) из общих групп без дублей. */
/* Собрать storage-узлы (storage=yes в adm_tags) из общих групп без дублей. */
int dm_mailbox_get_storage_nodes(struct UTUN_INSTANCE* inst, uint64_t* out, int max) {
struct dm_mb_state* mb = mb_of(inst);
if (!inst || !out || max <= 0 || !mb || !mb->db) return 0;
@@ -291,7 +291,7 @@ int dm_mailbox_get_storage_nodes(struct UTUN_INSTANCE* inst, uint64_t* out, int
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
if (nid == my_id) continue;
const char* tags = (const char*)sqlite3_column_text(st, 1);
if (tags && json_flat_get(tags, "dm_storage", buf, sizeof(buf)) == 0
if (tags && json_flat_get(tags, "storage", buf, sizeof(buf)) == 0
&& strcmp(buf, "yes") == 0) {
int dup = 0;
for (int i = 0; i < found; i++) if (out[i] == nid) { dup = 1; break; }
+3 -3
View File
@@ -2,7 +2,7 @@
* dm_mailbox.h — offline-хранение DM-сообщений на storage-узлах
*
* Когда пир недоступен (нет прямой связи), отправитель кладёт зашифрованное
* сообщение на storage-узлы (мемберы общих групп с флагом dm_storage=yes).
* сообщение на storage-узлы (мемберы общих групп с флагом storage=yes).
* Получатель при выходе в online вытягивает накопившуюся почту, проверяет
* подпись, расшифровывает и вставляет в dm_messages (dedup по seq).
*
@@ -39,8 +39,8 @@ int dm_mailbox_put(struct UTUN_INSTANCE* inst, uint64_t recipient, uint64_t send
* всех известных storage-узлов. Вызывается из uasync-потока. */
void dm_mailbox_pull(struct UTUN_INSTANCE* inst, uint64_t peer_node_id, uint64_t since_seq);
/* Собрать node_id storage-узлов (dm_storage=yes) из общих групп. До max штук.
* Возвращает количество найденных. out — буфер на max*8 байт. */
/* Собрать node_id storage-узлов (storage=yes в adm_tags) из общих групп. До max
* штук. Возвращает количество найденных. out — буфер на max*8 байт. */
int dm_mailbox_get_storage_nodes(struct UTUN_INSTANCE* inst, uint64_t* out, int max);
#ifdef __cplusplus
+25 -2
View File
@@ -410,7 +410,8 @@ err_t tcp_bind(struct tcp_pcb *pcb, uint32_t ipaddr, uint16_t port)
if (pcb->state != CLOSED) return LERR_VAL;
ctx = pcb->ctx;
uint16_t new_port = port;
// Порт принимаем в network-order (конвенция вызывающих), дальше работаем в host-order.
uint16_t new_port = ntohs(port);
if (new_port == 0) {
new_port = tcp_new_port(pcb);
if (new_port == 0) return LERR_BUF;
@@ -429,7 +430,7 @@ err_t tcp_bind(struct tcp_pcb *pcb, uint32_t ipaddr, uint16_t port)
}
if (ipaddr != 0) pcb->local_ip = ipaddr;
pcb->local_port = ntohs(new_port);
pcb->local_port = new_port;
TCP_REG(&ctx->bound_pcbs, pcb);
return LERR_OK;
}
@@ -528,6 +529,11 @@ void tcp_recved(struct tcp_pcb *pcb, uint16_t len)
wnd_inflation = tcp_update_rcv_ann_wnd(pcb);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG,
"LWIP_RECVED lport=%u rport=%u len=%u rcv_wnd=%u rcv_ann=%u infl=%u wnd_upd=%d",
pcb->local_port, pcb->remote_port, len, pcb->rcv_wnd, pcb->rcv_ann_wnd,
(unsigned)wnd_inflation, wnd_inflation >= TCP_WND_UPDATE_THRESHOLD ? 1 : 0);
if (wnd_inflation >= TCP_WND_UPDATE_THRESHOLD) {
tcp_ack_now(pcb);
tcp_output(pcb);
@@ -640,6 +646,7 @@ void tcp_slowtmr(struct lwip_tcp_ctx *ctx)
ctx->slowtmr_ctr++;
// ---- process active PCBs ----
tcp_slowtmr_start:
prev = NULL;
pcb = ctx->active_pcbs;
@@ -670,6 +677,10 @@ void tcp_slowtmr(struct lwip_tcp_ctx *ctx)
if (pcb->persist_cnt >= backoff_cnt) {
int next_slot = 1;
if (pcb->snd_wnd == 0) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG,
"LWIP_PERSIST lport=%u rport=%u snd_wnd=0 probe=%u unsent=%d snd_buf=%u",
pcb->local_port, pcb->remote_port, pcb->persist_probe,
tcp_seg_count(pcb->unsent), (unsigned)tcp_sndbuf(pcb));
if (tcp_zero_window_probe(pcb) != LERR_OK) next_slot = 0;
} else {
if (tcp_split_unsent_seg(pcb, (uint16_t)pcb->snd_wnd) == LERR_OK) {
@@ -690,6 +701,11 @@ void tcp_slowtmr(struct lwip_tcp_ctx *ctx)
if (pcb->rtime >= pcb->rto) {
lwip_tcp_trace_record(pcb->ctx, 'T', pcb->snd_nxt, 0, pcb->cwnd,
(uint16_t)pcb->rto, pcb->rtime, (uint8_t)pcb->state);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG,
"LWIP_RTO lport=%u rport=%u st=%s rtime=%d rto=%d nrtx=%u cwnd=%u snd_wnd=%u aq=%d",
pcb->local_port, pcb->remote_port, tcp_debug_state_str(pcb->state),
pcb->rtime, pcb->rto, pcb->nrtx, pcb->cwnd, pcb->snd_wnd,
tcp_seg_count(pcb->unacked));
if ((tcp_rexmit_rto_prepare(pcb) == LERR_OK) || ((pcb->unacked == NULL) && (pcb->unsent != NULL))) {
if (pcb->state != SYN_SENT) {
uint8_t backoff_idx = LWIP_MIN(pcb->nrtx, sizeof(tcp_backoff) - 1);
@@ -767,7 +783,9 @@ void tcp_slowtmr(struct lwip_tcp_ctx *ctx)
(uint8_t)pcb2->local_port);
tcp_free(pcb2);
ctx->active_pcbs_changed = 0;
TCP_EVENT_ERR(last_state, err_fn, err_arg, LERR_ABRT);
if (ctx->active_pcbs_changed) goto tcp_slowtmr_start;
} else {
prev = pcb;
pcb = pcb->next;
@@ -776,7 +794,9 @@ void tcp_slowtmr(struct lwip_tcp_ctx *ctx)
if (prev->polltmr >= prev->pollinterval) {
prev->polltmr = 0;
err = LERR_OK;
ctx->active_pcbs_changed = 0;
TCP_EVENT_POLL(prev, err);
if (ctx->active_pcbs_changed) goto tcp_slowtmr_start;
if (err == LERR_OK) {
tcp_output(prev);
}
@@ -840,6 +860,7 @@ void tcp_fasttmr(struct lwip_tcp_ctx *ctx)
ctx->slowtmr_ctr++;
tcp_fasttmr_start:
pcb = ctx->active_pcbs;
while (pcb != NULL) {
if (pcb->last_timer != ctx->slowtmr_ctr) {
@@ -859,7 +880,9 @@ void tcp_fasttmr(struct lwip_tcp_ctx *ctx)
next = pcb->next;
if (pcb->refused_data != NULL) {
ctx->active_pcbs_changed = 0;
tcp_process_refused_data(pcb);
if (ctx->active_pcbs_changed) goto tcp_fasttmr_start;
}
pcb = next;
} else {
+1
View File
@@ -212,6 +212,7 @@ struct lwip_tcp_ctx {
uint32_t ticks;
uint8_t slowtmr_ctr; // for last_timer check (incremented by fasttmr and slowtmr)
uint8_t tmr_phase; // for even/odd slowtmr decision (incremented in tcp_tmr_cb)
uint8_t active_pcbs_changed; // коллбэк изменил список active_pcbs — перезапустить обход
uint16_t iss_seed;
uint16_t ip_id;
uint16_t port_seed;
+21 -3
View File
@@ -113,6 +113,13 @@ static void pbuf_realloc_local(struct pbuf *p, uint16_t new_len)
p->tot_len = new_len;
}
int tcp_seg_count(const struct tcp_seg *seg)
{
int n = 0;
while (seg) { n++; seg = seg->next; }
return n;
}
// ============================================================
// tcp_input — main entry point
// ============================================================
@@ -266,6 +273,15 @@ void lwip_tcp_input(struct lwip_tcp_ctx *ctx, struct pbuf *p,
recv_flags = 0;
recv_acked = 0;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG,
"LWIP_IN lport=%u rport=%u st=%s seq=%u ack=%u flags=%02x len=%u "
"rcv_wnd=%u rcv_ann=%u snd_wnd=%u snd_buf=%u cwnd=%u uq=%d aq=%d oq=%d",
pcb->local_port, pcb->remote_port, tcp_debug_state_str(pcb->state),
(unsigned)seqno, (unsigned)ackno, (unsigned)flags, (unsigned)tcplen,
pcb->rcv_wnd, pcb->rcv_ann_wnd, pcb->snd_wnd, (unsigned)tcp_sndbuf(pcb),
pcb->cwnd, tcp_seg_count(pcb->unsent),
tcp_seg_count(pcb->unacked), tcp_seg_count(pcb->ooseq));
if (flags & TCP_PSH) {
p->flags |= PBUF_FLAG_PUSH;
}
@@ -330,6 +346,11 @@ void lwip_tcp_input(struct lwip_tcp_ctx *ctx, struct pbuf *p,
tcp_input_pcb = NULL;
if (tcp_input_delayed_close(pcb)) goto aborted;
tcp_output(pcb);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG,
"LWIP_OUT lport=%u rport=%u st=%s rcv_wnd=%u rcv_ann=%u snd_wnd=%u snd_buf=%u cwnd=%u acked=%u fin=%d",
pcb->local_port, pcb->remote_port, tcp_debug_state_str(pcb->state),
pcb->rcv_wnd, pcb->rcv_ann_wnd, pcb->snd_wnd, (unsigned)tcp_sndbuf(pcb),
pcb->cwnd, (unsigned)recv_acked, (recv_flags & TF_GOT_FIN) ? 1 : 0);
}
}
aborted:
@@ -704,7 +725,6 @@ static void tcp_receive(struct tcp_pcb *pcb)
{
int16_t m;
uint32_t right_wnd_edge;
int found_dupack = 0;
if (flags & TCP_ACK) {
right_wnd_edge = pcb->snd_wnd + pcb->snd_wl2;
@@ -724,7 +744,6 @@ static void tcp_receive(struct tcp_pcb *pcb)
if (pcb->snd_wl2 + pcb->snd_wnd == right_wnd_edge) {
if (pcb->rtime >= 0) {
if (pcb->lastack == ackno) {
found_dupack = 1;
if ((uint8_t)(pcb->dupacks + 1) > pcb->dupacks)
++pcb->dupacks;
if (pcb->dupacks > 3)
@@ -735,7 +754,6 @@ static void tcp_receive(struct tcp_pcb *pcb)
}
}
}
if (!found_dupack) pcb->dupacks = 0;
} else if (TCP_SEQ_BETWEEN(ackno, pcb->lastack + 1, pcb->snd_nxt)) {
tcpwnd_size_t acked;
+23 -6
View File
@@ -623,6 +623,9 @@ static uint16_t
tcp_pseudo_checksum(uint32_t src_ip, uint32_t dst_ip, uint8_t proto, uint16_t tcp_len, const struct pbuf *p)
{
uint32_t sum = 0;
uint8_t leftover = 0;
int have_leftover = 0;
const struct pbuf *q;
sum += (uint32_t)ntohs((uint16_t)((src_ip >> 16) & 0xFFFF));
sum += (uint32_t)ntohs((uint16_t)(src_ip & 0xFFFF));
@@ -631,15 +634,27 @@ tcp_pseudo_checksum(uint32_t src_ip, uint32_t dst_ip, uint8_t proto, uint16_t tc
sum += (uint32_t)(uint16_t)proto;
sum += (uint32_t)tcp_len;
const struct pbuf *q;
// Суммируем цепочку pbuf как непрерывный поток байт: нечётный хвост одного
// pbuf должен пароваться с первым байтом следующего, а не дополняться нулём
// по месту (иначе чексумма неверна для конкатенированных сегментов с нечётной
// длиной не-последнего pbuf).
for (q = p; q != NULL; q = q->next) {
const uint8_t *b = (const uint8_t *)q->payload;
uint16_t remaining = q->len;
uint16_t i;
for (i = 0; i + 1 < remaining; i += 2)
uint16_t i = 0;
if (have_leftover && remaining > 0) {
sum += (uint32_t)(((uint16_t)leftover << 8) | b[0]);
i = 1;
have_leftover = 0;
}
for (; i + 1 < remaining; i += 2)
sum += (uint32_t)((uint16_t)(b[i] << 8) | b[i + 1]);
if (remaining & 1) sum += (uint32_t)((uint16_t)b[remaining - 1] << 8);
if (i < remaining) {
leftover = b[i];
have_leftover = 1;
}
}
if (have_leftover) sum += (uint32_t)((uint16_t)leftover << 8);
while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16);
return (uint16_t)~sum;
@@ -796,9 +811,11 @@ tcp_rexmit_fast(struct tcp_pcb *pcb)
{
if (pcb == NULL) return;
if (pcb->unacked != NULL && !(pcb->flags & TF_INFR)) {
uint32_t rseq = ntohl(pcb->unacked->tcphdr->seqno);
uint16_t rlen = pcb->unacked->len;
if (tcp_rexmit(pcb) == LERR_OK) {
lwip_tcp_trace_record(pcb->ctx, 'F', ntohl(pcb->unacked->tcphdr->seqno),
pcb->unacked->len, pcb->ssthresh, (uint16_t)pcb->rto, pcb->rtime, (uint8_t)pcb->state);
lwip_tcp_trace_record(pcb->ctx, 'F', rseq,
rlen, pcb->ssthresh, (uint16_t)pcb->rto, pcb->rtime, (uint8_t)pcb->state);
pcb->ssthresh = pcb->cwnd;
if ((uint32_t)pcb->snd_wnd < pcb->cwnd) pcb->ssthresh = pcb->snd_wnd;
pcb->ssthresh = (tcpwnd_size_t)(pcb->ssthresh / 2);
+6 -2
View File
@@ -186,6 +186,10 @@ PACK_STRUCT_END
extern const uint8_t tcp_backoff[13];
extern const uint8_t tcp_persist_backoff[7];
// state name (для логов)
const char *tcp_debug_state_str(enum tcp_state s);
int tcp_seg_count(const struct tcp_seg *seg);
// Internal functions
err_t tcp_send_empty_ack(struct tcp_pcb *pcb);
err_t tcp_send_fin(struct tcp_pcb *pcb);
@@ -247,8 +251,8 @@ void lwip_tcp_stats_clear(struct lwip_tcp_ctx *ctx);
(npcb)->next = NULL; \
} while(0)
#define TCP_REG_ACTIVE(ctx, npcb) TCP_REG(&(ctx)->active_pcbs, npcb)
#define TCP_RMV_ACTIVE(ctx, npcb) TCP_RMV(&(ctx)->active_pcbs, npcb)
#define TCP_REG_ACTIVE(ctx, npcb) do { TCP_REG(&(ctx)->active_pcbs, npcb); (ctx)->active_pcbs_changed = 1; } while(0)
#define TCP_RMV_ACTIVE(ctx, npcb) do { TCP_RMV(&(ctx)->active_pcbs, npcb); (ctx)->active_pcbs_changed = 1; } while(0)
#ifdef __cplusplus
+59 -42
View File
@@ -19,70 +19,87 @@
#include <dirent.h>
#endif
struct media_async {
int initialized;
};
struct ma_thread_ctx;
struct media_async { struct ma_thread_ctx* jobs; int closing; };
struct ma_thread_ctx {
struct media_async* owner;
struct ma_thread_ctx* next;
struct UASYNC* ua;
ma_work_fn work;
void* data;
ma_done_fn done;
void* arg;
pthread_t thread;
void* timer;
ma_work_fn work;
void* data;
ma_done_fn done;
void* arg;
int finished;
};
static void ma_done_trampoline(void* raw) {
struct ma_thread_ctx* ctx = (struct ma_thread_ctx*)raw;
ctx->done(ctx->arg, 0);
u_free(ctx);
static void ma_finish(struct ma_thread_ctx* job, int error) {
struct ma_thread_ctx** p = &job->owner->jobs;
while (*p && *p != job) p = &(*p)->next;
if (*p) *p = job->next;
pthread_join(job->thread, NULL);
job->done(job->arg, error);
u_free(job);
}
static void* ma_thread_entry(void* raw) {
struct ma_thread_ctx* ctx = (struct ma_thread_ctx*)raw;
ctx->work(ctx->data);
uasync_post(ctx->ua, ma_done_trampoline, ctx);
static void ma_poll(void* arg) {
struct ma_thread_ctx* job = arg; job->timer = NULL;
if (__atomic_load_n(&job->finished, __ATOMIC_ACQUIRE)) { ma_finish(job, 0); return; }
job->timer = uasync_set_timeout(job->ua, 100, job, ma_poll, "media_worker");
if (!job->timer) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media worker completion timer failed");
ma_finish(job, -1);
}
}
static void* ma_thread_entry(void* arg) {
struct ma_thread_ctx* job = arg;
job->work(job->data);
__atomic_store_n(&job->finished, 1, __ATOMIC_RELEASE);
uasync_wakeup(job->ua);
return NULL;
}
struct media_async* media_async_create(void) {
struct media_async* ma = u_malloc(sizeof(*ma));
if (!ma) return NULL;
ma->initialized = 1;
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "media_async: created");
struct media_async* ma = u_calloc(1, sizeof(*ma));
if (!ma) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media worker owner allocation failed");
else DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "media_async: created");
return ma;
}
void media_async_destroy(struct media_async* ma) {
if (!ma) return;
ma->initialized = 0;
ma->closing = 1;
unsigned cancelled = 0;
while (ma->jobs) {
struct ma_thread_ctx* job = ma->jobs;
if (job->timer) uasync_cancel_timeout(job->ua, job->timer);
ma_finish(job, MEDIA_ASYNC_CANCELLED); cancelled++;
}
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "media_async: destroyed cancelled=%u", cancelled);
u_free(ma);
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "media_async: destroyed");
}
void media_async_submit(struct media_async* ma, struct UASYNC* ua,
ma_work_fn work, void* data,
ma_done_fn done, void* arg) {
(void)ma;
if (!ua || !work || !done) return;
struct ma_thread_ctx* ctx = u_malloc(sizeof(*ctx));
if (!ctx) { done(arg, -1); return; }
ctx->ua = ua;
ctx->work = work;
ctx->data = data;
ctx->done = done;
ctx->arg = arg;
pthread_t tid;
int rc = pthread_create(&tid, NULL, ma_thread_entry, ctx);
if (rc != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_async: pthread_create failed rc=%d", rc);
u_free(ctx);
done(arg, -1);
ma_work_fn work, void* data, ma_done_fn done, void* arg) {
if (!ma || !ua || !work || !done || ma->closing) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media task rejected: invalid arguments or stopping owner");
if (done) done(arg, MEDIA_ASYNC_CANCELLED);
return;
}
pthread_detach(tid);
struct ma_thread_ctx* job = u_calloc(1, sizeof(*job));
if (!job) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media task allocation failed"); done(arg, -1); return; }
job->owner = ma; job->ua = ua; job->work = work; job->data = data; job->done = done; job->arg = arg;
job->timer = uasync_set_timeout(ua, 1, job, ma_poll, "media_worker");
if (!job->timer) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media task timer failed"); u_free(job); done(arg, -1); return; }
int rc = pthread_create(&job->thread, NULL, ma_thread_entry, job);
if (rc) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media task thread failed rc=%d", rc);
uasync_cancel_timeout(ua, job->timer); u_free(job); done(arg, -1); return;
}
job->next = ma->jobs; ma->jobs = job;
}
/* ─── примитивы (вызываются из worker thread) ─── */
+14 -3
View File
@@ -1,4 +1,7 @@
// media_async.h — асинхронный движок thread-per-task + примитивы (хеш, подпись, копия файла)
/* media_async — владелец фоновых задач обработки файлов и криптографии.
* Каждая work выполняется в отдельном pthread; done — в потоке uasync.
* create/submit/destroy вызываются из этого потока. destroy ждёт workers и
* завершает ожидающие done с CANCELLED, пока данные вызывающего ещё существуют. */
#ifndef MEDIA_ASYNC_H
#define MEDIA_ASYNC_H
@@ -13,21 +16,29 @@ struct UASYNC;
struct media_async;
typedef void (*ma_work_fn)(void* data);
typedef void (*ma_done_fn)(void* arg, int err);
typedef void (*ma_work_fn)(void* data); // рабочий поток; не обращаться к состоянию uasync
typedef void (*ma_done_fn)(void* arg, int err); // 0=work завершён, -1=ошибка запуска/таймера, -2=отмена
/* Создать владельца задач; NULL при ошибке выделения памяти. */
struct media_async* media_async_create(void);
#define MEDIA_ASYNC_CANCELLED (-2)
/* Вызывать вне done. Ждёт работы, вызывает ожидающие done и освобождает ma; NULL допустим. */
void media_async_destroy(struct media_async* ma);
/* Передать задачу; data/arg остаются у вызывающего до done. При ошибке done
* может быть вызван синхронно. Ошибку самой work передают через data, а не err. */
void media_async_submit(struct media_async* ma, struct UASYNC* ua,
ma_work_fn work, void* data,
ma_done_fn done, void* arg);
/* Синхронные примитивы для worker: SHA256 файла, Ed25519-подпись, копирование. 0/-1. */
int ma_sha256_file(const char* path, uint8_t hash_out[32]);
int ma_sign_block(const uint8_t* ed25519_privkey,
const uint8_t* data, size_t len, uint8_t sig_out[64]);
int ma_copy_file(const char* src, const char* dst);
/* Размер файла в байтах (int; для файлов, помещающихся в этот тип), -1 при ошибке stat. */
int ma_file_size(const char* path);
/* Заполнить 16 случайных байт идентификатора. */
void ma_uuid(uint8_t uuid_out[16]);
/* Рекурсивный обход каталога: для каждого regular-файла вызывается cb(full_path, size).
+72 -33
View File
@@ -23,6 +23,7 @@
/* ── forward decls ── */
static void md_relay_cancel_block(struct media_delivery_ctx* md, struct relay_block_ctx* block);
static void md_etcp_recv(struct ETCP_CONN* conn, struct ll_entry* entry);
static void md_on_conn_status(struct ETCP_CONN* conn, int status, void* arg);
static void md_on_bgp_node(struct TOPO_GROUP* group, uint64_t node_id, int event, void* arg);
@@ -266,6 +267,7 @@ void md_relay_remove(struct media_delivery_ctx* md, const uint8_t* block_id) {
if (!md->relay_blocks) return;
struct ll_entry* e = queue_find_data_by_index(md->relay_blocks, block_id);
if (!e) return;
md_relay_cancel_block(md, (struct relay_block_ctx*)e->data);
queue_remove_data(md->relay_blocks, e);
queue_entry_free(e);
}
@@ -410,9 +412,6 @@ static int md_send(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_n
if (!inst || !data || len == 0) return -1;
struct TOPO_GROUP* grp = inst->topo_groups ? topo_groups_find(inst->topo_groups, group_id) : NULL;
struct ETCP_CONN* conn = grp ? topo_group_find_conn_for_node(grp, dst_node_id) : NULL;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "%s: send dst=0x%016llx group=0x%016llx subcmd=0x%02x len=%zu conn=%p",
MD_ID, (unsigned long long)dst_node_id, (unsigned long long)group_id,
(unsigned)data[0], len, (void*)conn);
if (!conn)
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "%s: no conn to 0x%016llx group=0x%016llx subcmd=0x%02x — packet queued, no route",
MD_ID, (unsigned long long)dst_node_id, (unsigned long long)group_id, (unsigned)data[0]);
@@ -720,6 +719,7 @@ static void md_super_conn_cb(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64
#define MD_CHUNK_SIZE 1024
struct stream_ctx {
struct stream_ctx* next;
struct media_delivery_ctx* md;
uint64_t group_id;
uint64_t dst_node_id;
@@ -734,18 +734,30 @@ struct stream_ctx {
struct queue_waiter_handle waiter;
};
static void stream_free(struct stream_ctx* sc) {
struct media_delivery_ctx* md = sc->md;
etcp_router_cancel_send_ready(md->inst, sc->group_id, sc->dst_node_id, ETCP_RT_ID_MEDIA_DELIVERY, &sc->waiter);
struct stream_ctx** p = &md->streams;
while (*p && *p != sc) p = &(*p)->next;
if (*p) *p = sc->next;
md_file_load_dec(md, sc->media_id, sc->dst_node_id);
media_delivery_stream_done(md->inst);
if (sc->file) fclose(sc->file);
u_free(sc);
}
static void stream_send_chunk_cb(struct ll_queue* q, void* arg) {
(void)q;
struct stream_ctx* sc = (struct stream_ctx*)arg;
struct media_delivery_ctx* md = sc->md;
if (!md || sc->remaining == 0) { u_free(sc); return; }
if (!md->initialized || sc->remaining == 0) { stream_free(sc); return; }
while (sc->remaining > 0) {
size_t to_read = sc->remaining < MD_CHUNK_SIZE ? (size_t)sc->remaining : MD_CHUNK_SIZE;
uint8_t buf[MD_CHUNK_SIZE];
fseeko(sc->file, (off_t)sc->offset, SEEK_SET);
size_t rd = fread(buf, 1, to_read, sc->file);
if (rd == 0) { u_free(sc); return; }
if (rd == 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "stream read failed"); stream_free(sc); return; }
uint8_t pkt[MEDIA_BLOCK_CHUNK_HDR_SIZE + MD_CHUNK_SIZE];
struct media_pkt_block_chunk* ch = (struct media_pkt_block_chunk*)pkt;
@@ -762,10 +774,7 @@ static void stream_send_chunk_cb(struct ll_queue* q, void* arg) {
if (snd != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "%s: stream chunk send FAILED rc=%d — abort stream chunk=%d to 0x%016llx",
MD_ID, snd, sc->chunk, (unsigned long long)sc->dst_node_id);
md_file_load_dec(md, sc->media_id, sc->dst_node_id);
media_delivery_stream_done(md->inst);
fclose(sc->file);
u_free(sc);
stream_free(sc);
return;
}
@@ -805,10 +814,7 @@ static void stream_send_chunk_cb(struct ll_queue* q, void* arg) {
bd->total_size = (uint32_t)sc->block_data_len;
md_send(md->inst, sc->group_id, sc->dst_node_id, done_pkt, MEDIA_BLOCK_DONE_SIZE);
md_file_load_dec(md, sc->media_id, sc->dst_node_id);
media_delivery_stream_done(md->inst);
fclose(sc->file);
u_free(sc);
stream_free(sc);
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "%s: BLOCK_DONE sent chunk=%d total=%u",
MD_ID, chunk, bd->total_size);
}
@@ -817,6 +823,7 @@ static void stream_send_chunk_cb(struct ll_queue* q, void* arg) {
/* ── relay chunk forwarding ── */
struct relay_fwd_ctx {
struct relay_fwd_ctx* next;
struct media_delivery_ctx* md;
uint64_t dst_node_id;
uint64_t group_id;
@@ -825,16 +832,33 @@ struct relay_fwd_ctx {
struct queue_waiter_handle waiter;
};
static void relay_fwd_free(struct relay_fwd_ctx* fc) {
etcp_router_cancel_send_ready(fc->md->inst, fc->group_id, fc->dst_node_id, ETCP_RT_ID_MEDIA_DELIVERY, &fc->waiter);
struct relay_fwd_ctx** p = &fc->md->forwards;
while (*p && *p != fc) p = &(*p)->next;
if (*p) *p = fc->next;
u_free(fc);
}
static void md_relay_cancel_block(struct media_delivery_ctx* md, struct relay_block_ctx* block) {
struct relay_fwd_ctx* fc = md->forwards;
while (fc) {
struct relay_fwd_ctx* next = fc->next;
if (fc->rc == block) relay_fwd_free(fc);
fc = next;
}
}
static void md_relay_fwd_cb(struct ll_queue* q, void* arg) {
(void)q;
struct relay_fwd_ctx* fc = (struct relay_fwd_ctx*)arg;
struct relay_block_ctx* rc = fc->rc;
struct relay_downstream* ds = &rc->downstream[fc->ds_idx];
if (ds->sent_offset >= rc->file_offset) { u_free(fc); return; }
if (ds->sent_offset >= rc->file_offset) { relay_fwd_free(fc); return; }
FILE* f = fopen(rc->chunk_file, "rb");
if (!f) { u_free(fc); return; }
if (!f) { relay_fwd_free(fc); return; }
fseeko(f, (off_t)ds->sent_offset, SEEK_SET);
size_t to_read = rc->file_offset - ds->sent_offset;
@@ -842,7 +866,7 @@ static void md_relay_fwd_cb(struct ll_queue* q, void* arg) {
uint8_t buf[1024];
size_t rd = fread(buf, 1, to_read, f);
fclose(f);
if (rd == 0) { u_free(fc); return; }
if (rd == 0) { relay_fwd_free(fc); return; }
uint8_t pkt[MEDIA_BLOCK_CHUNK_HDR_SIZE + 1024];
struct media_pkt_block_chunk* ch = (struct media_pkt_block_chunk*)pkt;
@@ -865,7 +889,7 @@ static void md_relay_fwd_cb(struct ll_queue* q, void* arg) {
fc->dst_node_id, ETCP_RT_ID_MEDIA_DELIVERY,
&fc->waiter, md_relay_fwd_cb, fc);
} else {
u_free(fc);
relay_fwd_free(fc);
}
}
@@ -876,6 +900,7 @@ static void md_relay_catchup(struct media_delivery_ctx* md, struct relay_block_c
struct relay_fwd_ctx* fc = u_calloc(1, sizeof(*fc));
if (!fc) return;
fc->next = md->forwards; md->forwards = fc;
fc->md = md; fc->dst_node_id = dst_node_id; fc->group_id = group_id;
fc->rc = rc; fc->ds_idx = ds_idx;
memset(&fc->waiter, 0, sizeof(fc->waiter));
@@ -1029,7 +1054,7 @@ static void md_handle_block_req(struct media_delivery_ctx* md, uint64_t from_nod
struct stream_ctx* sc = u_calloc(1, sizeof(*sc));
if (!sc) { fclose(f); md->active_streams--; md_file_load_dec(md, req->media_id, from_node); return; }
sc->md = md;
sc->md = md; sc->next = md->streams; md->streams = sc;
sc->group_id = group_id;
sc->dst_node_id = from_node;
memcpy(sc->media_id, req->media_id, 16);
@@ -1235,17 +1260,20 @@ static void md_on_conn_status(struct ETCP_CONN* conn, int status, void* arg) {
/* ── main dispatch ── */
static void md_etcp_recv(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (!conn || !entry || !entry->dgram || entry->len < 1) return;
if (!conn || !entry || !entry->dgram || entry->len < 1) {
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); }
return;
}
struct UTUN_INSTANCE* inst = conn->instance;
if (!inst) return;
if (!inst) { queue_dgram_free(entry); queue_entry_free(entry); return; }
struct media_delivery_ctx* md = &inst->md;
if (!md->initialized) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "%s: recv before init", MD_ID); return; }
if (!md->initialized) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "%s: recv before init", MD_ID); queue_dgram_free(entry); queue_entry_free(entry); return; }
const uint8_t* data = entry->dgram;
size_t len = entry->len;
/* Единый формат router_deliver: [svc_id][src_node_id(8)][dst_node_id(8)][subcmd][...] */
if (len < ROUTER_SVC_PAYLOAD_OFF + 1) return;
if (len < ROUTER_SVC_PAYLOAD_OFF + 1) { queue_dgram_free(entry); queue_entry_free(entry); return; }
uint64_t from_node;
memcpy(&from_node, data + ROUTER_SVC_SRC_OFF, 8);
uint8_t subcmd = data[ROUTER_SVC_PAYLOAD_OFF];
@@ -1318,6 +1346,7 @@ static void md_etcp_recv(struct ETCP_CONN* conn, struct ll_entry* entry) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "%s: unknown subcmd 0x%02x from 0x%016llx",
MD_ID, subcmd, (unsigned long long)from_node);
}
queue_dgram_free(entry); queue_entry_free(entry);
}
/* ── table creation ── */
@@ -1414,6 +1443,10 @@ void media_delivery_destroy(struct UTUN_INSTANCE* inst) {
if (!inst || !inst->md.initialized) return;
struct media_delivery_ctx* md = &inst->md;
md->initialized = 0;
while (md->streams) stream_free(md->streams);
while (md->forwards) relay_fwd_free(md->forwards);
etcp_router_unbind(inst, ETCP_RT_ID_MEDIA_DELIVERY);
etcp_remove_conn_status_cbk(inst, md_on_conn_status, md);
member_sync_remove_props_cbk(inst, md_on_props_changed, md);
@@ -1439,18 +1472,11 @@ void media_delivery_destroy(struct UTUN_INSTANCE* inst) {
}
if (md->served_nodes) { queue_free(md->served_nodes); md->served_nodes = NULL; }
if (md->downloads) {
struct ll_entry* de = md->downloads->head;
while (de) {
struct media_download* dl = (struct media_download*)de->data;
if (dl->watchdog_timer) { uasync_cancel_timeout(md->inst->ua, dl->watchdog_timer); dl->watchdog_timer = NULL; }
for (int pi = 0; pi < dl->num_peers; pi++)
if (dl->peers[pi].cm_handle) { conn_mgr_close(dl->peers[pi].cm_handle); dl->peers[pi].cm_handle = NULL; }
if (dl->blocks) u_free(dl->blocks);
if (dl->block_sigs) u_free(dl->block_sigs);
de = de->next;
while (md->downloads->head) {
struct media_download* dl = (struct media_download*)md->downloads->head->data;
media_download_cancel(inst, dl->media_id, NULL, 0);
}
queue_free(md->downloads);
md->downloads = NULL;
queue_free(md->downloads); md->downloads = NULL;
}
if (md->relay_blocks) { queue_free(md->relay_blocks); md->relay_blocks = NULL; }
if (md->file_loads) { queue_free(md->file_loads); md->file_loads = NULL; }
@@ -1463,6 +1489,19 @@ void media_delivery_cancel_channel(struct UTUN_INSTANCE* inst, uint64_t group_id
if (!inst || !inst->md.initialized) return;
struct media_delivery_ctx* md = &inst->md;
struct stream_ctx* stream = md->streams;
while (stream) {
struct stream_ctx* next = stream->next;
if (stream->group_id == group_id) stream_free(stream);
stream = next;
}
struct relay_fwd_ctx* forward = md->forwards;
while (forward) {
struct relay_fwd_ctx* next = forward->next;
if (forward->group_id == group_id) relay_fwd_free(forward);
forward = next;
}
/* 1. собрать media_id активных загрузок этого канала и отменить их */
uint8_t media_ids[MEDIA_MAX_DOWNLOADS][16];
int nm = 0;
+4
View File
@@ -105,7 +105,11 @@ struct relay_block_ctx {
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_*
+2 -4
View File
@@ -89,9 +89,6 @@ static int md_dl_send(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t ds
if (!inst || !inst->topo_groups || !inst->connections) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: md_dl_send: no routing (inst=%p topo=%p conn=%p)", MDL_ID, (void*)inst, (void*)(inst ? inst->topo_groups : NULL), (void*)(inst ? inst->connections : NULL)); return -1; }
{ struct TOPO_GROUP* grp = topo_groups_find(inst->topo_groups, group_id);
struct ETCP_CONN* gc = grp ? topo_group_find_conn_for_node(grp, dst) : NULL;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "%s: send dst=0x%016llx group=0x%016llx subcmd=0x%02x len=%zu grp=%p conn=%p",
MDL_ID, (unsigned long long)dst, (unsigned long long)group_id,
(unsigned)data[0], len, (void*)grp, (void*)gc);
if (!gc)
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: no conn to 0x%016llx subcmd=0x%02x — packet queued, no route",
MDL_ID, (unsigned long long)dst, (unsigned)data[0]);
@@ -267,7 +264,7 @@ int md_dl_check_stall(struct media_download* dl, uint64_t now_tb) {
if (b->state != MD_BLK_REQ && b->state != MD_BLK_RECV) continue;
if (now_tb - b->last_progress_tb >= stall) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: STALL blk=%d state=%d no progress %llums",
MDL_ID, bi, b->state, (unsigned long long)(now_tb - b->last_progress_tb));
MDL_ID, bi, b->state, (unsigned long long)((now_tb - b->last_progress_tb) / 10));
if (md_dl_failover_block(dl, bi, now_tb) < 0) return -1;
}
}
@@ -357,6 +354,7 @@ void md_dl_conn_cb(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_i
if (b->state == MD_BLK_CONN && b->holder_idx >= 0 && b->holder_idx < b->num_holders
&& b->holders[b->holder_idx] == node_id) {
b->state = MD_BLK_REQ;
b->last_progress_tb = get_time_tb(); /* соединение только поднялось — обнуляем простой */
md_dl_send_block_req(dl->inst, node_id, dl, bi);
}
}
Loaded 100 of 389 files, more files were not shown because too many files have changed in this diff. Show more